From dd0bf45b74429dac59eb11c3847d7d774361cff6 Mon Sep 17 00:00:00 2001 From: PsyanticY Date: Fri, 14 Feb 2020 18:01:07 +0100 Subject: [PATCH] aws: add aws-ec2-launch-template resource --- nix/aws-ec2-launch-template.nix | 100 +++++++ nix/common-ec2-instance-options.nix | 168 +++++++++++ nix/default.nix | 1 + nix/ec2.nix | 181 +----------- nixopsaws/resources/__init__.py | 1 + .../resources/aws_ec2_launch_template.py | 278 ++++++++++++++++++ 6 files changed, 564 insertions(+), 165 deletions(-) create mode 100644 nix/aws-ec2-launch-template.nix create mode 100644 nix/common-ec2-instance-options.nix create mode 100644 nixopsaws/resources/aws_ec2_launch_template.py diff --git a/nix/aws-ec2-launch-template.nix b/nix/aws-ec2-launch-template.nix new file mode 100644 index 00000000..6c6f6116 --- /dev/null +++ b/nix/aws-ec2-launch-template.nix @@ -0,0 +1,100 @@ +{ config, lib, uuid, name, ... }: + +with import ./lib.nix lib; +with lib; + +{ + imports = [ ./common-ec2-auth-options.nix ]; + + options = { + + templateName = mkOption { + default = "nixops-${uuid}-${name}"; + type = types.str; + description = "Name of the launch template."; + }; + + templateId = mkOption { + default = ""; + type = types.str; + description = "ec2 launch template ID (set by NixOps)"; + }; + + versionDescription = mkOption { + default = ""; + type = types.str; + description = "A description for the version of the launch template"; + }; + + + # we might want to make this in a way similar to ec2.nix + ebsOptimized = mkOption { + default = true; + description = '' + Whether the EC2 instance should be created as an EBS Optimized instance. + ''; + type = types.bool; + }; + + userData = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The user data to make available to the instance. + It should be valid nix expressions. + ''; + }; + + # add support for ec2 then move to common + disableApiTermination = mkOption { + default = false; + type = types.bool; + description = '' + If set to true , you can't terminate the instance + using the Amazon EC2 console, CLI, or API. + ''; + }; + + # add support for ec2 then move to common + instanceInitiatedShutdownBehavior = mkOption { + default = "terminate"; + type = types.enum ["stop" "terminate"]; + description = '' + Indicates whether an instance stops or terminates + when you initiate shutdown from the instance (using + the operating system command for system shutdown). + ''; + }; + # add support for ec2 then move to common + networkInterfaceId = mkOption { + default = ""; + # must get the id fro mthe name + type = with types; either str (resource "vpc-network-interface"); + apply = x: if builtins.isString x then x else "res-" + x._name "." + x._type; + description = '' + The ID of the network interface. + ''; + }; + + privateIpAddresses = mkOption { + default = null; + type = with types; (nullOr (listOf str)); + description = '' + One or more secondary private IPv4 addresses. + ''; + }; + secondaryPrivateIpAddressCount = mkOption { + default = null; + type = types.nullOr types.int; + description = '' + The number of secondary private IPv4 addresses to assign to a network interface. + When you specify a number of secondary IPv4 addresses, Amazon EC2 selects these + IP addresses within the subnet's IPv4 CIDR range. + You can't specify this option and specify privateIpAddresses in the same time. + ''; + }; + + }// (import ./common-ec2-options.nix { inherit lib; }) // (import ./common-ec2-instance-options.nix { inherit lib; }); + + config._type = "aws-ec2-launch-template"; +} \ No newline at end of file diff --git a/nix/common-ec2-instance-options.nix b/nix/common-ec2-instance-options.nix new file mode 100644 index 00000000..a4e85976 --- /dev/null +++ b/nix/common-ec2-instance-options.nix @@ -0,0 +1,168 @@ +# Options shared between an ec2 resource type and the +# launch template resource in EC2 +# instances. + +{ lib }: + +with lib; +with import ./lib.nix lib; +{ + + zone = mkOption { + default = ""; + example = "us-east-1c"; + type = types.str; + description = '' + The EC2 availability zone in which the instance should be + created. If not specified, a zone is selected automatically. + ''; + }; + + # add support for ec2 + monitoring = mkOption { + default = false; + type = types.bool; + description = '' + if set to true, detailed monitoring is enabled. + Otherwise, basic monitoring is enabled. + ''; + }; + + tenancy = mkOption { + default = "default"; + type = types.enum [ "default" "dedicated" "host" ]; + description = '' + The tenancy of the instance (if the instance is running in a VPC). + An instance with a tenancy of dedicated runs on single-tenant hardware. + An instance with host tenancy runs on a Dedicated Host, which is an + isolated server with configurations that you can control. + ''; + }; + + ebsInitialRootDiskSize = mkOption { + default = 0; + type = types.int; + description = '' + Preferred size (G) of the root disk of the EBS-backed instance. By + default, EBS-backed images have a size determined by the + AMI. Only supported on creation of the instance. + ''; + }; + + ami = mkOption { + example = "ami-00000000"; + type = types.str; + description = '' + EC2 identifier of the AMI disk image used in the virtual + machine. This must be a NixOS image providing SSH access. + ''; + }; + + instanceType = mkOption { + default = "m1.small"; + example = "m1.large"; + type = types.str; + description = '' + EC2 instance type. See for a + list of valid Amazon EC2 instance types. + ''; + }; + + instanceProfile = mkOption { + default = ""; + example = "rolename"; + type = types.str; + description = '' + The name of the IAM Instance Profile (IIP) to associate with + the instances. + ''; + }; + + keyPair = mkOption { + example = "my-keypair"; + type = types.either types.str (resource "ec2-keypair"); + apply = x: if builtins.isString x then x else x.name; + description = '' + Name of the SSH key pair to be used to communicate securely + with the instance. Key pairs can be created using the + ec2-add-keypair command. + ''; + }; + + securityGroupIds = mkOption { + default = [ "default" ]; + type = types.listOf types.str; + description = '' + Security Group IDs for the instance. Necessary if starting + an instance inside a VPC/subnet. In the non-default VPC, security + groups needs to be specified by ID and not name. + ''; + }; + + subnetId = mkOption { + default = ""; + example = "subnet-00000000"; + type = types.either types.str (resource "vpc-subnet"); + apply = x: if builtins.isString x then x else "res-" + x._name + "." + x._type; + description = '' + The subnet inside a VPC to launch the instance in. + ''; + }; + + associatePublicIpAddress = mkOption { + default = false; + type = types.bool; + description = '' + If instance in a subnet/VPC, whether to associate a public + IP address with the instance. + ''; + }; + + placementGroup = mkOption { + default = ""; + example = "my-cluster"; + type = types.either types.str (resource "ec2-placement-group"); + apply = x: if builtins.isString x then x else x.name; + description = '' + Placement group for the instance. + ''; + }; + + spotInstancePrice = mkOption { + default = 0; + type = types.int; + description = '' + Price (in dollar cents per hour) to use for spot instances request for the machine. + If the value is equal to 0 (default), then spot instances are not used. + ''; + }; + + spotInstanceRequestType = mkOption { + default = "one-time"; + type = types.enum [ "one-time" "persistent" ]; + description = '' + The type of the spot instance request. It can be either "one-time" or "persistent". + ''; + }; + + spotInstanceInterruptionBehavior = mkOption { + default = "terminate"; + type = types.enum [ "terminate" "stop" "hibernate" ]; + description = '' + Whether to terminate, stop or hibernate the instance when it gets interrupted. + For stop, spotInstanceRequestType must be set to "persistent". + ''; + }; + + spotInstanceTimeout = mkOption { + default = 0; + type = types.int; + description = '' + The duration (in seconds) that the spot instance request is + valid. If the request cannot be satisfied in this amount of + time, the request will be cancelled automatically, and NixOps + will fail with an error message. The default (0) is no timeout. + ''; + }; +} \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index fd71ca4f..d3547d71 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -44,6 +44,7 @@ awsVPNGateways = evalResources ./aws-vpn-gateway.nix (zipAttrs resourcesByType.awsVPNGateways or []); awsVPNConnections = evalResources ./aws-vpn-connection.nix (zipAttrs resourcesByType.awsVPNConnections or []); awsVPNConnectionRoutes = evalResources ./aws-vpn-connection-route.nix (zipAttrs resourcesByType.awsVPNConnectionRoutes or []); + awsEc2LaunchTemplate = evalResources ./aws-ec2-launch-template.nix (zipAttrs resourcesByType.awsEc2LaunchTemplate or []); }; } diff --git a/nix/ec2.nix b/nix/ec2.nix index b78de953..6efe6a2e 100644 --- a/nix/ec2.nix +++ b/nix/ec2.nix @@ -173,9 +173,9 @@ in ###### interface - options = { + options.deployment.ec2 = { - deployment.ec2.accessKeyId = mkOption { + accessKeyId = mkOption { default = ""; example = "AKIABOGUSACCESSKEY"; type = types.str; @@ -197,7 +197,7 @@ in ''; }; - deployment.ec2.region = mkOption { + region = mkOption { default = ""; example = "us-east-1"; type = types.str; @@ -208,28 +208,7 @@ in ''; }; - deployment.ec2.zone = mkOption { - default = ""; - example = "us-east-1c"; - type = types.str; - description = '' - The EC2 availability zone in which the instance should be - created. If not specified, a zone is selected automatically. - ''; - }; - - deployment.ec2.tenancy = mkOption { - default = "default"; - type = types.enum [ "default" "dedicated" "host" ]; - description = '' - The tenancy of the instance (if the instance is running in a VPC). - An instance with a tenancy of dedicated runs on single-tenant hardware. - An instance with host tenancy runs on a Dedicated Host, which is an - isolated server with configurations that you can control. - ''; - }; - - deployment.ec2.ebsBoot = mkOption { + ebsBoot = mkOption { default = true; type = types.bool; description = '' @@ -241,37 +220,7 @@ in ''; }; - deployment.ec2.ebsInitialRootDiskSize = mkOption { - default = 0; - type = types.int; - description = '' - Preferred size (G) of the root disk of the EBS-backed instance. By - default, EBS-backed images have a size determined by the - AMI. Only supported on creation of the instance. - ''; - }; - - deployment.ec2.ami = mkOption { - example = "ami-00000000"; - type = types.str; - description = '' - EC2 identifier of the AMI disk image used in the virtual - machine. This must be a NixOS image providing SSH access. - ''; - }; - - deployment.ec2.instanceType = mkOption { - default = "m1.small"; - example = "m1.large"; - type = types.str; - description = '' - EC2 instance type. See for a - list of valid Amazon EC2 instance types. - ''; - }; - - deployment.ec2.instanceId = mkOption { + instanceId = mkOption { default = ""; type = types.str; description = '' @@ -279,28 +228,7 @@ in ''; }; - deployment.ec2.instanceProfile = mkOption { - default = ""; - example = "rolename"; - type = types.str; - description = '' - The name of the IAM Instance Profile (IIP) to associate with - the instances. - ''; - }; - - deployment.ec2.keyPair = mkOption { - example = "my-keypair"; - type = types.either types.str (resource "ec2-keypair"); - apply = x: if builtins.isString x then x else x.name; - description = '' - Name of the SSH key pair to be used to communicate securely - with the instance. Key pairs can be created using the - ec2-add-keypair command. - ''; - }; - - deployment.ec2.privateKey = mkOption { + privateKey = mkOption { default = ""; example = "/home/alice/.ssh/id_rsa-my-keypair"; type = types.str; @@ -314,7 +242,7 @@ in ''; }; - deployment.ec2.securityGroups = mkOption { + securityGroups = mkOption { default = [ "default" ]; example = [ "my-group" "my-other-group" ]; type = types.listOf (types.either types.str (resource "ec2-security-group")); @@ -325,36 +253,7 @@ in ''; }; - deployment.ec2.securityGroupIds = mkOption { - default = [ "default" ]; - type = types.listOf types.str; - description = '' - Security Group IDs for the instance. Necessary if starting - an instance inside a VPC/subnet. In the non-default VPC, security - groups needs to be specified by ID and not name. - ''; - }; - - deployment.ec2.subnetId = mkOption { - default = ""; - example = "subnet-00000000"; - type = types.either types.str (resource "vpc-subnet"); - apply = x: if builtins.isString x then x else "res-" + x._name + "." + x._type; - description = '' - The subnet inside a VPC to launch the instance in. - ''; - }; - - deployment.ec2.associatePublicIpAddress = mkOption { - default = false; - type = types.bool; - description = '' - If instance in a subnet/VPC, whether to associate a public - IP address with the instance. - ''; - }; - - deployment.ec2.usePrivateIpAddress = mkOption { + usePrivateIpAddress = mkOption { default = defaultUsePrivateIpAddress; type = types.bool; description = '' @@ -365,7 +264,7 @@ in ''; }; - deployment.ec2.sourceDestCheck = mkOption { + sourceDestCheck = mkOption { default = true; type = types.bool; description = '' @@ -374,19 +273,9 @@ in ''; }; - deployment.ec2.placementGroup = mkOption { - default = ""; - example = "my-cluster"; - type = types.either types.str (resource "ec2-placement-group"); - apply = x: if builtins.isString x then x else x.name; - description = '' - Placement group for the instance. - ''; - }; - - deployment.ec2.tags = commonEC2Options.tags; + tags = commonEC2Options.tags; - deployment.ec2.blockDeviceMapping = mkOption { + blockDeviceMapping = mkOption { default = { }; example = { "/dev/xvdb".disk = "ephemeral0"; "/dev/xvdg".disk = "vol-00000000"; }; type = with types; attrsOf (submodule ec2DiskOptions); @@ -408,7 +297,7 @@ in ''; }; - deployment.ec2.elasticIPv4 = mkOption { + elasticIPv4 = mkOption { default = ""; example = "123.1.123.123"; type = types.either types.str (resource "elastic-ip"); @@ -418,7 +307,7 @@ in ''; }; - deployment.ec2.physicalProperties = mkOption { + physicalProperties = mkOption { default = {}; example = { cores = 4; memory = 14985; }; description = '' @@ -427,44 +316,7 @@ in ''; }; - deployment.ec2.spotInstancePrice = mkOption { - default = 0; - type = types.int; - description = '' - Price (in dollar cents per hour) to use for spot instances request for the machine. - If the value is equal to 0 (default), then spot instances are not used. - ''; - }; - - deployment.ec2.spotInstanceRequestType = mkOption { - default = "one-time"; - type = types.enum [ "one-time" "persistent" ]; - description = '' - The type of the spot instance request. It can be either "one-time" or "persistent". - ''; - }; - - deployment.ec2.spotInstanceInterruptionBehavior = mkOption { - default = "terminate"; - type = types.enum [ "terminate" "stop" "hibernate" ]; - description = '' - Whether to terminate, stop or hibernate the instance when it gets interrupted. - For stop, spotInstanceRequestType must be set to "persistent". - ''; - }; - - deployment.ec2.spotInstanceTimeout = mkOption { - default = 0; - type = types.int; - description = '' - The duration (in seconds) that the spot instance request is - valid. If the request cannot be satisfied in this amount of - time, the request will be cancelled automatically, and NixOps - will fail with an error message. The default (0) is no timeout. - ''; - }; - - deployment.ec2.ebsOptimized = mkOption { + ebsOptimized = mkOption { default = defaultEbsOptimized; type = types.bool; description = '' @@ -472,10 +324,9 @@ in ''; }; - fileSystems = mkOption { + } // import ./common-ec2-instance-options.nix { inherit lib; }; + options.fileSystems = mkOption { type = with types; loaOf (submodule fileSystemsOptions); - }; - }; diff --git a/nixopsaws/resources/__init__.py b/nixopsaws/resources/__init__.py index 20773100..d578a47d 100644 --- a/nixopsaws/resources/__init__.py +++ b/nixopsaws/resources/__init__.py @@ -36,3 +36,4 @@ import vpc_route_table import vpc_route_table_association import vpc_subnet +import aws_ec2_launch_template \ No newline at end of file diff --git a/nixopsaws/resources/aws_ec2_launch_template.py b/nixopsaws/resources/aws_ec2_launch_template.py new file mode 100644 index 00000000..e6877233 --- /dev/null +++ b/nixopsaws/resources/aws_ec2_launch_template.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +import ast +import sys +import base64 +import nixops.util +import nixopsaws.ec2_utils +import nixops.resources +import botocore.exceptions +from ec2_common import EC2CommonState + +class awsEc2LaunchTemplateDefinition(nixops.resources.ResourceDefinition): + """Definition of an ec2 launch template""" + + @classmethod + def get_type(cls): + return "aws-ec2-launch-template" + + @classmethod + def get_resource_type(cls): + return "awsEc2LaunchTemplate" + + def show_type(self): + return "{0}".format(self.get_type()) + +class awsEc2LaunchTemplateState(nixops.resources.ResourceState, EC2CommonState): + """State of an ec2 launch template""" + + state = nixops.util.attr_property("state", nixops.resources.ResourceState.MISSING, int) + access_key_id = nixops.util.attr_property("accessKeyId", None) + region = nixops.util.attr_property("region", None) + templateName = nixops.util.attr_property("templateName", None) + templateId = nixops.util.attr_property("templateId", None) + templateVersion = nixops.util.attr_property("templateVersion", None) + versionDescription = nixops.util.attr_property("versionDescription", None) + ebsOptimized = nixops.util.attr_property("ebsOptimized", True, type=bool) + instanceProfile = nixops.util.attr_property("instanceProfile", None) + ami = nixops.util.attr_property("ami", None) + instanceType = nixops.util.attr_property("instanceType", None) + keyPair = nixops.util.attr_property("keyPair", None) + userData = nixops.util.attr_property("userData", None) + securityGroupIds = nixops.util.attr_property("securityGroupIds", None, 'json') + disableApiTermination = nixops.util.attr_property("disableApiTermination", False, type=bool) + instanceInitiatedShutdownBehavior = nixops.util.attr_property("instanceInitiatedShutdownBehavior", None) + placementGroup = nixops.util.attr_property("placementGroup", None) + zone = nixops.util.attr_property("zone", None) + tenancy = nixops.util.attr_property("tenancy", None) + associatePublicIpAddress = nixops.util.attr_property("associatePublicIpAddress", True, type=bool) + networkInterfaceId = nixops.util.attr_property("networkInterfaceId", None) + subnetId = nixops.util.attr_property("subnetId", None) + privateIpAddresses = nixops.util.attr_property("privateIpAddresses", {}, 'json') + secondaryPrivateIpAddressCount = nixops.util.attr_property("secondaryPrivateIpAddressCount", None) + monitoring = nixops.util.attr_property("LTMonitoring", False, type=bool) + spotInstancePrice = nixops.util.attr_property("ec2.spotInstancePrice", None) + spotInstanceRequestType = nixops.util.attr_property("spotInstanceRequestType", None) + spotInstanceInterruptionBehavior = nixops.util.attr_property("spotInstanceInterruptionBehavior", None) + spotInstanceTimeout = nixops.util.attr_property("spotInstanceTimeout", None) + clientToken = nixops.util.attr_property("clientToken", None) + ebsInitialRootDiskSize = nixops.util.attr_property("ebsInitialRootDiskSize", None) + + @classmethod + def get_type(cls): + return "aws-ec2-launch-template" + + def __init__(self, depl, name, id): + nixops.resources.ResourceState.__init__(self, depl, name, id) + self._conn_boto3 = None + self._conn_vpc = None + + def _exists(self): + return self.state != self.MISSING + + def show_type(self): + s = super(awsEc2LaunchTemplateState, self).show_type() + return s + + @property + def resource_id(self): + return self.templateId + + def connect_boto3(self, region): + if self._conn_boto3: return self._conn_boto3 + self._conn_boto3 = nixopsaws.ec2_utils.connect_ec2_boto3(region, self.access_key_id) + return self._conn_boto3 + + def connect_vpc(self): + if self._conn_vpc: + return self._conn_vpc + self._conn_vpc = nixopsaws.ec2_utils.connect_vpc(self.region, self.access_key_id) + return self._conn_vpc + + def _update_tag(self, defn): + self.connect_boto3(self.region) + tags = defn.config['tags'] + tags.update(self.get_common_tags()) + self._conn_boto3.create_tags(Resources=[self.templateId], Tags=[{"Key": k, "Value": tags[k]} for k in tags]) + + # TODO: Work on how to update the template (create a new version and update default version to use or what) + # i think this is done automatically so i think i need to remove it right ? + def create_after(self, resources, defn): + # EC2 launch templates can require key pairs, IAM roles, security + # groups and placement groups + return {r for r in resources if + isinstance(r, nixopsaws.resources.ec2_keypair.EC2KeyPairState) or + isinstance(r, nixopsaws.resources.iam_role.IAMRoleState) or + isinstance(r, nixopsaws.resources.ec2_security_group.EC2SecurityGroupState) or + isinstance(r, nixopsaws.resources.ec2_placement_group.EC2PlacementGroupState) or + isinstance(r, nixopsaws.resources.vpc_subnet.VPCSubnetState)} + + # fix security group stuff later + def security_groups_to_ids(self, subnetId, groups): + sg_names = filter(lambda g: not g.startswith('sg-'), groups) + if sg_names != [ ] and subnetId != "": + self.connect_vpc() + vpc_id = self._conn_vpc.get_all_subnets([subnetId])[0].vpc_id + + #Note: we can use ec2_utils.name_to_security_group but it only works with boto2 + group_ids = [] + for i in groups: + if i.startswith('sg-'): + group_ids.append(i) + else: + try: + group_ids.append(self._conn_boto3.describe_security_groups(Filters=[{'Name': 'group-name', + 'Values': [i]}] + )['SecurityGroups'][0]['GroupId']) + except botocore.exceptions.ClientError as error: + raise error + return group_ids + else: + return groups + + def create(self, defn, check, allow_reboot, allow_recreate): + + if self.region is None: + self.region = defn.config['region'] + elif self.region != defn.config['region']: + self.warn("cannot change region of a running instance (from ‘{}‘ to ‘{}‘)" + .format(self.region, defn.config['region'])) + + self.access_key_id = defn.config['accessKeyId'] + self.connect_boto3(self.region) + if self.state != self.UP: + tags = defn.config['tags'] + tags.update(self.get_common_tags()) + args = dict() + args['LaunchTemplateName'] = defn.config['templateName'] + args['VersionDescription'] = defn.config['versionDescription'] + args['LaunchTemplateData'] = dict( + EbsOptimized=defn.config['ebsOptimized'], + ImageId=defn.config['ami'], + Placement=dict(Tenancy=defn.config['tenancy']), + Monitoring=dict(Enabled=defn.config['monitoring']), + DisableApiTermination=defn.config['disableApiTermination'], + InstanceInitiatedShutdownBehavior=defn.config['instanceInitiatedShutdownBehavior'], + TagSpecifications=[dict( + ResourceType='instance', + Tags=[{"Key": k, "Value": tags[k]} for k in tags] + ), + dict( + ResourceType='volume', + Tags=[{"Key": k, "Value": tags[k]} for k in tags] + ) ] + ) + if defn.config['instanceProfile'] != "": + args['LaunchTemplateData']['IamInstanceProfile'] = dict( + Name=defn.config['instanceProfile'] + ) + if defn.config['userData']: + args['LaunchTemplateData']['UserData'] = base64.b64encode(defn.config['userData']) + + if defn.config['instanceType']: + args['LaunchTemplateData']['InstanceType'] = defn.config['instanceType'] + if defn.config['placementGroup'] != "": + args['LaunchTemplateData']['Placement']['GroupName'] = defn.config['placementGroup'] + if defn.config['zone']: + args['LaunchTemplateData']['Placement']['AvailabilityZone'] = defn.config['zone'] + + if defn.config['spotInstancePrice'] != 0: + args['LaunchTemplateData']['InstanceMarketOptions'] = dict( + MarketType="spot", + SpotOptions=dict( + MaxPrice=str(defn.config['spotInstancePrice']/100.0), + SpotInstanceType=defn.config['spotInstanceRequestType'], + ValidUntil=(datetime.datetime.utcnow() + + datetime.timedelta(0, defn.config['spotInstanceTimeout'])).isoformat(), + InstanceInterruptionBehavior=defn.config['spotInstanceInterruptionBehavior'] + ) + ) + if defn.config['networkInterfaceId'] != "" or defn.config['subnetId'] != "": + + args['LaunchTemplateData']['NetworkInterfaces'] = [dict( + DeviceIndex=0, + AssociatePublicIpAddress=defn.config['associatePublicIpAddress'] + )] + if defn.config['securityGroupIds']!=[]: + args['LaunchTemplateData']['NetworkInterfaces'][0]['Groups'] = self.security_groups_to_ids(defn.config['subnetId'], defn.config['securityGroupIds']) + if defn.config['networkInterfaceId'] != "": + if defn.config['networkInterfaceId'].startswith("res-"): + res = self.depl.get_typed_resource(defn.config['networkInterfaceId'][4:].split(".")[0], "vpc-network-interface") + defn.config['networkInterfaceId'] = res._state['networkInterfaceId'] + args['LaunchTemplateData']['NetworkInterfaces'][0]['networkInterfaceId']=defn.config['networkInterfaceId'] + if defn.config['subnetId'] != "": + if defn.config['subnetId'].startswith("res-"): + res = self.depl.get_typed_resource(defn.config['subnetId'][4:].split(".")[0], "vpc-subnet") + defn.config['subnetId'] = res._state['subnetId'] + args['LaunchTemplateData']['NetworkInterfaces'][0]['SubnetId']=defn.config['subnetId'] + if defn.config['secondaryPrivateIpAddressCount']: + args['LaunchTemplateData']['NetworkInterfaces'][0]['SecondaryPrivateIpAddressCount']=defn.config['secondaryPrivateIpAddressCount'] + if defn.config['privateIpAddresses']: + args['LaunchTemplateData']['NetworkInterfaces'][0]['PrivateIpAddresses']=defn.config['privateIpAddresses'] + if defn.config['keyPair'] != "": + args['LaunchTemplateData']['KeyName']=defn.config['keyPair'] + + ami = self._conn_boto3.describe_images(ImageIds=[defn.config['ami']])['Images'][0] + + # TODO: BlockDeviceMappings for non root volumes + args['LaunchTemplateData']['BlockDeviceMappings'] = [dict( + DeviceName="/dev/sda1", + Ebs=dict( + DeleteOnTermination=True, + VolumeSize=defn.config['ebsInitialRootDiskSize'], + VolumeType=ami['BlockDeviceMappings'][0]['Ebs']['VolumeType'] + ) + )] + # Use a client token to ensure that the template creation is + # idempotent; i.e., if we get interrupted before recording + # the fleet ID, we'll get the same fleet ID on the + # next run. + if not self.clientToken: + with self.depl._db: + self.clientToken = nixops.util.generate_random_string(length=48) # = 64 ASCII chars + self.state = self.STARTING + + args['ClientToken'] = self.clientToken + self.log("creating launch template {} ...".format(defn.config['templateName'])) + try: + launch_template = self._conn_boto3.create_launch_template(**args) + except botocore.exceptions.ClientError as error: + raise error + # Not sure whether to use lambda retry or keep it like this + with self.depl._db: + self.templateId = launch_template['LaunchTemplate']['LaunchTemplateId'] + self.templateName = defn.config['templateName'] + self.templateVersion = launch_template['LaunchTemplate']['LatestVersionNumber'] + self.versionDescription = defn.config['versionDescription'] + self.state = self.UP + # these are the tags for the template + self._update_tag(defn) + + def check(self): + + self.connect_boto3(self.region) + launch_template = self._conn_boto3.describe_launch_templates( + LaunchTemplateIds=[self.templateId] + )['LaunchTemplates'] + if launch_template is None: + self.state = self.MISSING + return + if str(launch_template[0]['DefaultVersionNumber']) != self.templateVersion: + self.warn("default version on the launch template is different then nixops managed version...") + + def _destroy(self): + + self.connect_boto3(self.region) + self.log("deleting ec2 launch template `{}`... ".format(self.templateName)) + try: + self._conn_boto3.delete_launch_template(LaunchTemplateId=self.templateId) + except botocore.exceptions.ClientError as error: + if error.response['Error']['Code'] == "InvalidLaunchTemplateId.NotFound": + self.warn("Template `{}` already deleted...".format(self.templateName)) + else: + raise error + + def destroy(self, wipe=False): + if not self._exists(): return True + + self._destroy() + return True