Updates to enable launch of EC2 instances via user-defined templates (#274)
* Updates to enable launch of EC2 instances that use
EBS-backed (non-ephemeral) data storage, etc, via
user-defined EC2 request templates
* - Removed redundant MinCount/MaxCount updates from
init_request method in Ec2Cluster base class
- Updated muchos.props.example to improve documentation
for the new cluster_template variable
* PR feedback addressed:
- Added conf/templates/user/.gitignore, removed
conf/templates/.gitignore
- Renamed .devices file to 'devices' and updated README
Other updates:
- Renamed 'cluster_template' dict field to 'cluster_template_d'
for consistency with other dictionaries in the class
- Removed 'cluster_template_id' field, added that info to
the dictionary instead
- Fixed typo(s) in README.md
* README.md fix
diff --git a/conf/muchos.props.example b/conf/muchos.props.example
index 9932c40..599c812 100644
--- a/conf/muchos.props.example
+++ b/conf/muchos.props.example
@@ -62,6 +62,9 @@
# Type of AWS instance launched for any node running 'worker' service
# Leave default below to use same instance type set by 'default_instance_type' property
worker_instance_type = %(default_instance_type)s
+# Enable template mode by selecting a template from conf/templates, in order to leverage your own
+# custom EC2 launch requests (optional). See conf/templates/README.md for more information
+#cluster_template = example
# VPC to launch instances in (optional)
#vpc_id = vpc-xxxxx
# VPC Subnet to launch instances in (optional)
diff --git a/conf/templates/README.md b/conf/templates/README.md
new file mode 100644
index 0000000..235d5be
--- /dev/null
+++ b/conf/templates/README.md
@@ -0,0 +1,188 @@
+## Muchos EC2 Cluster Templates
+
+Cluster templates are intended to provide greater flexibility, if needed,
+with respect to instance type selection and launch configuration for
+your EC2 hosts. For example, cluster templates may be ideal for use
+cases that require distinct, per-host launch configurations, and
+for use cases that require hosts to have persistent, EBS-backed data
+volumes (rather than ephemeral volumes, the muchos default for EC2
+clusters)
+
+If you are already familiar with muchos and with the basics of EC2
+launch requests, then creating your own launch templates will be simple
+and straightforward.
+
+Please follow the guidance provided here to ensure compatibility between
+your custom templates and muchos automation
+
+## Configuration
+
+### Select a cluster template in *muchos.props*
+```
+[ec2]
+...
+cluster_template = example
+...
+```
+The configured value must match the name of a subdirectory under
+`conf/templates`
+
+```
+~$ ls -1a fluo-muchos/conf/templates/example
+.
+..
+devices
+metrics.json
+namenode.json
+resourcemanager.json
+zookeeper.json
+worker.json
+```
+The subdirectory will contain one or more user-defined EC2 launch
+templates (*\*.json*) for your various host types, and it will
+include a *devices* file specifying the desired mount points for all
+data volumes (excluding root volumes, as they are mounted
+automatically)
+
+### Defining EC2 launch templates and device mounts for your hosts
+
+#### Launch Templates: *{service-name}.json* files
+
+Each JSON file represents a standard EC2 launch request, and each file
+name must match one of the predefined muchos service names, as
+defined in the **nodes** section of *muchos.props*. E.g.,
+```
+...
+[nodes]
+leader1 = namenode,resourcemanager,accumulomaster
+leader2 = metrics,zookeeper
+worker1 = worker
+worker2 = worker
+worker3 = worker
+worker4 = worker
+```
+In template mode, the first service listed for a given host denotes the
+template to be selected for its launch.
+
+Based on the example given above:
+* **leader1** selects **namenode.json**
+* **leader2** selects **metrics.json**
+* **worker1** selects **worker.json**
+* and so on...
+
+For example, *namenode.json* might be defined as follows...
+```
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1", # Here, /dev/sda1 denotes the root volume
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf", # Here, /dev/sdf (a.k.a "/dev/xvdf") denotes
+ "Ebs": { # our single EBS-backed data volume
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.4xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
+```
+*Property Placeholders*
+
+The `${property name}` placeholders demonstrated above are optional and
+are intended to simplify template creation and reduce maintenance burden.
+They allow any matching properties from the **ec2** section of *muchos.props*
+to be interpolated automatically.
+
+If needed, you may also define your own custom properties and have them be injected
+automatically by simply adding them to the **ec2** section of
+*muchos.props* and to your templates
+
+#### Device Mounts: *devices* file
+
+The *devices* file contains the user-defined mapping of storage
+devices and mount points for all data (i.e., non-root) volumes in your
+cluster.
+
+Two (and only two) device mappings should exist within *devices*:
+* One map to represent your **worker** device mounts, and
+* One map to represent the device mounts on all other hosts, i.e., the
+ **default** map
+
+For example, the *devices* file below specifies 4 mount points for all
+`worker` instance types, and specifies 1 mount point for all
+other hosts via the `default` map.
+```
+{
+ "default": {
+ "mounts": [
+ "/data0" # For non-workers, mount the /data0 directory
+ ], # on /dev/xvdf (a.k.a "/dev/sdf")
+ "devices": [
+ "/dev/xvdf"
+ ]
+ },
+ "worker": {
+ "mounts": [
+ "/data0",
+ "/data1",
+ "/data2",
+ "/data3" # For workers, mount the /data0 directory on
+ ], # /dev/xvdf (a.k.a "/dev/sdf"), mount /data1 on
+ "devices": [ # /dev/xvdg (a.k.a "/dev/sdg"), and so on...
+ "/dev/xvdf",
+ "/dev/xvdg",
+ "/dev/xvdh",
+ "/dev/xvdi"
+ ]
+ }
+}
+```
+Naturally, you should take care to ensure that your **BlockDeviceMappings**
+also align to *default* vs *worker* node type semantics. As you
+explore the example files, you should observe the implicit link betweeen
+a data volume denoted by *BlockDeviceMappings\[N].DeviceName* and its
+respective device map entry in the *devices* file.
+
+* **Note**: While *DeviceName* mount profiles should not vary among
+ your default (non-worker) nodes, other attributes within *BlockDeviceMappings*,
+ such as *DeleteOnTermination*, *VolumeSize*, *etc*, may vary as needed
+
+* **Note**: Be aware that the device names used by AWS EC2 launch requests
+ may differ from the actual names assigned by the EC2 block device driver, which
+ can be confusing. Please read
+ [this](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html)
+ and [also this](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
+ for additional information and guidance, if needed
+
+* **Note**: Root EBS volumes may be configured as desired in your JSON
+ launch templates, but only non-root, "data" storage devices should be
+ specified in *devices*, as root devices are mounted automatically
+
+
+## Beyond the Launch Phase: *Setup*, *Terminate*, *Etc*
+
+Aside from the configuration differences described above, which impact
+the `launch` phase, all other muchos operations behave the same in
+template mode as in default mode
+
diff --git a/conf/templates/example/accumulomaster.json b/conf/templates/example/accumulomaster.json
new file mode 100644
index 0000000..263c192
--- /dev/null
+++ b/conf/templates/example/accumulomaster.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.2xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/client.json b/conf/templates/example/client.json
new file mode 100644
index 0000000..c5d4de1
--- /dev/null
+++ b/conf/templates/example/client.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.2xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/devices b/conf/templates/example/devices
new file mode 100644
index 0000000..f338f9f
--- /dev/null
+++ b/conf/templates/example/devices
@@ -0,0 +1,24 @@
+{
+ "default": {
+ "mounts": [
+ "/data0"
+ ],
+ "devices": [
+ "/dev/xvdf"
+ ]
+ },
+ "worker": {
+ "mounts": [
+ "/data0",
+ "/data1",
+ "/data2",
+ "/data3"
+ ],
+ "devices": [
+ "/dev/xvdf",
+ "/dev/xvdg",
+ "/dev/xvdh",
+ "/dev/xvdi"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/conf/templates/example/metrics.json b/conf/templates/example/metrics.json
new file mode 100644
index 0000000..c5d4de1
--- /dev/null
+++ b/conf/templates/example/metrics.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.2xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/namenode.json b/conf/templates/example/namenode.json
new file mode 100644
index 0000000..b281782
--- /dev/null
+++ b/conf/templates/example/namenode.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.4xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/resourcemanager.json b/conf/templates/example/resourcemanager.json
new file mode 100644
index 0000000..a406feb
--- /dev/null
+++ b/conf/templates/example/resourcemanager.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.2xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/worker.json b/conf/templates/example/worker.json
new file mode 100644
index 0000000..ae693ee
--- /dev/null
+++ b/conf/templates/example/worker.json
@@ -0,0 +1,58 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 250,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdg",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 250,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdh",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 250,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdi",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 250,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "c4.4xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/example/zookeeper.json b/conf/templates/example/zookeeper.json
new file mode 100644
index 0000000..16ae67e
--- /dev/null
+++ b/conf/templates/example/zookeeper.json
@@ -0,0 +1,34 @@
+{
+ "KeyName": "${key_name}",
+ "BlockDeviceMappings": [
+ {
+ "DeviceName": "/dev/sda1",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 40,
+ "VolumeType": "gp2"
+ }
+ },
+ {
+ "DeviceName": "/dev/sdf",
+ "Ebs": {
+ "DeleteOnTermination": true,
+ "VolumeSize": 500,
+ "VolumeType": "gp2"
+ }
+ }
+ ],
+ "ImageId": "${aws_ami}",
+ "InstanceType": "m4.xlarge",
+ "NetworkInterfaces": [
+ {
+ "DeviceIndex": 0,
+ "AssociatePublicIpAddress": ${associate_public_ip},
+ "Groups": [
+ "${security_group_id}"
+ ]
+ }
+ ],
+ "EbsOptimized": true,
+ "InstanceInitiatedShutdownBehavior": "${shutdown_behavior}"
+}
diff --git a/conf/templates/user/.gitignore b/conf/templates/user/.gitignore
new file mode 100644
index 0000000..f59ec20
--- /dev/null
+++ b/conf/templates/user/.gitignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
diff --git a/lib/main.py b/lib/main.py
index 383c242..62df08c 100644
--- a/lib/main.py
+++ b/lib/main.py
@@ -49,7 +49,9 @@
hosts_path = join(hosts_dir, opts.cluster)
- config = DeployConfig(deploy_path, config_path, hosts_path, checksums_path, opts.cluster)
+ templates_path = join(deploy_path, "conf/templates/")
+
+ config = DeployConfig(deploy_path, config_path, hosts_path, checksums_path, templates_path, opts.cluster)
config.verify_config(action)
if action == 'config':
@@ -64,8 +66,11 @@
cluster = ExistingCluster(config)
cluster.perform(action)
elif cluster_type == 'ec2':
- from muchos.ec2 import Ec2Cluster
- cluster = Ec2Cluster(config)
+ from muchos.ec2 import Ec2Cluster, Ec2ClusterTemplate
+ if config.has_option('ec2', 'cluster_template'):
+ cluster = Ec2ClusterTemplate(config)
+ else:
+ cluster = Ec2Cluster(config)
cluster.perform(action)
else:
exit('Unknown cluster_type: ' + cluster_type)
diff --git a/lib/muchos/config.py b/lib/muchos/config.py
index b0a3fe5..3b07c27 100644
--- a/lib/muchos/config.py
+++ b/lib/muchos/config.py
@@ -19,6 +19,8 @@
from sys import exit
from .util import get_ephemeral_devices, get_arch
import os
+import json
+import glob
SERVICES = ['zookeeper', 'namenode', 'resourcemanager', 'accumulomaster', 'mesosmaster', 'worker', 'fluo', 'fluo_yarn', 'metrics', 'spark', 'client', 'swarmmanager']
@@ -27,7 +29,7 @@
class DeployConfig(ConfigParser):
- def __init__(self, deploy_path, config_path, hosts_path, checksums_path, cluster_name):
+ def __init__(self, deploy_path, config_path, hosts_path, checksums_path, templates_path, cluster_name):
ConfigParser.__init__(self)
self.optionxform = str
self.deploy_path = deploy_path
@@ -43,6 +45,8 @@
self.checksums_path = checksums_path
self.checksums_d = None
self.init_nodes()
+ self.cluster_template_d = None
+ self.init_template(templates_path)
def verify_config(self, action):
proxy = self.get('general', 'proxy_hostname')
@@ -86,6 +90,9 @@
return max((len(self.default_ephemeral_devices()), len(self.worker_ephemeral_devices())))
def node_type_map(self):
+ if self.cluster_template_d:
+ return self.cluster_template_d['devices']
+
node_types = {}
if self.get_cluster_type() == 'ec2':
node_list = [('default', self.default_ephemeral_devices()), ('worker', self.worker_ephemeral_devices())]
@@ -189,9 +196,10 @@
return self.checksums_d[key]
def verify_instance_type(self, instance_type):
- if get_arch(instance_type) == 'pvm':
- exit("ERROR - Configuration contains instance type '{0}' that uses pvm architecture."
- "Only hvm architecture is supported!".format(instance_type))
+ if not self.cluster_template_d:
+ if get_arch(instance_type) == 'pvm':
+ exit("ERROR - Configuration contains instance type '{0}' that uses pvm architecture."
+ "Only hvm architecture is supported!".format(instance_type))
def instance_tags(self):
retd = {}
@@ -272,8 +280,6 @@
else:
exit('ERROR - Bad line %s in hosts %s' % (line, self.hosts_path))
-
-
def get_hosts(self):
if self.hosts is None:
self.parse_hosts()
@@ -332,6 +338,77 @@
return
exit("Property '{0}' was not found".format(key))
+ def init_template(self, templates_path):
+ if not self.has_option('ec2', 'cluster_template'):
+ return
+
+ template_id = self.get('ec2', 'cluster_template')
+ for root, dirs, files in os.walk(templates_path):
+ for dir_name in dirs:
+ if dir_name == template_id:
+ self.cluster_template_d = {'id': template_id}
+ template_dir = os.path.join(root, dir_name)
+ self.load_template_ec2_requests(template_dir)
+ self.load_template_device_map(template_dir)
+ break
+ break
+
+ self.validate_template()
+
+ def load_template_ec2_requests(self, template_dir):
+ for json_path in glob.glob(os.path.join(template_dir, '*.json')):
+ service = os.path.basename(json_path).rsplit('.', 1)[0]
+ if service not in SERVICES:
+ exit("ERROR - Template '{0}' has unrecognized option '{1}'. Must be one of {2}".format(
+ self.cluster_template_d['id'], service, str(SERVICES)))
+ with open(json_path, 'r') as json_file:
+ # load as string, so we can use string.Template to inject config values
+ self.cluster_template_d[service] = json_file.read()
+
+ def load_template_device_map(self, template_dir):
+ device_map_path = os.path.join(template_dir, 'devices')
+ if not os.path.isfile(device_map_path):
+ exit("ERROR - template '{0}' is missing 'devices' config".format(self.cluster_template_d['id']))
+ with open(device_map_path, 'r') as json_file:
+ self.cluster_template_d['devices'] = json.load(json_file)
+
+ def validate_template(self):
+ if not self.cluster_template_d:
+ exit("ERROR - Template '{0}' is not defined!".format(self.cluster_template_d['id']))
+
+ if 'worker' not in self.cluster_template_d:
+ exit("ERROR - '{0}' template config is invalid. No 'worker' launch request is defined".format(
+ self.cluster_template_d['id']))
+
+ if 'worker' not in self.cluster_template_d['devices']:
+ exit("ERROR - '{0}' template is invalid. The devices file must have a 'worker' device map".format(
+ self.cluster_template_d['id']))
+
+ if 'default' not in self.cluster_template_d['devices']:
+ exit("ERROR - '{0}' template is invalid. The devices file must have a 'default' device map".format(
+ self.cluster_template_d['id']))
+
+ # Validate the selected launch template for each host
+
+ worker_count = 0
+ for hostname in self.node_d:
+ # first service listed denotes the selected template
+ selected_ec2_request = self.node_d[hostname][0]
+ if 'worker' == selected_ec2_request:
+ worker_count = worker_count + 1
+ else:
+ if 'worker' in self.node_d[hostname]:
+ exit("ERROR - '{0}' node config is invalid. The 'worker' service should be listed first".format(
+ hostname))
+ if selected_ec2_request not in self.cluster_template_d:
+ if len(self.node_d[hostname]) > 1:
+ print('Hint: In template mode, the first service listed for a host denotes its EC2 template')
+ exit("ERROR - '{0}' node config is invalid. No EC2 template defined for the '{1}' service".format(
+ hostname, selected_ec2_request))
+
+ if worker_count == 0:
+ exit("ERROR - No worker instances are defined for template '{0}'".format(self.cluster_template_d['id']))
+
HOST_VAR_DEFAULTS = {
'accumulo_home': '"{{ install_dir }}/accumulo-{{ accumulo_version }}"',
diff --git a/lib/muchos/ec2.py b/lib/muchos/ec2.py
index 57a354f..2973d87 100644
--- a/lib/muchos/ec2.py
+++ b/lib/muchos/ec2.py
@@ -24,6 +24,8 @@
import time
import boto3
from .existing import ExistingCluster
+import json
+from string import Template
class Ec2Cluster(ExistingCluster):
@@ -33,40 +35,17 @@
def launch_node(self, hostname, services, sg_id):
- associate_public_ip = True
- if self.config.has_option('ec2', 'associate_public_ip'):
- associate_public_ip = self.config.get('ec2', 'associate_public_ip').strip().lower() == 'true'
+ request = self.init_request(hostname, services, sg_id)
- request = {'MinCount': 1, 'MaxCount': 1,
- 'NetworkInterfaces': [{'DeviceIndex': 0, 'AssociatePublicIpAddress': associate_public_ip,
- 'Groups': [sg_id]}]}
-
- if self.config.has_option('ec2', 'subnet_id'):
- request['NetworkInterfaces'][0]['SubnetId'] = self.config.get('ec2', 'subnet_id')
-
- if 'worker' in services:
- instance_type = self.config.get('ec2', 'worker_instance_type')
- else:
- instance_type = self.config.get('ec2', 'default_instance_type')
- request['InstanceType'] = instance_type
- request['InstanceInitiatedShutdownBehavior'] = self.config.get('ec2', 'shutdown_behavior')
-
- if not self.config.has_option('ec2', 'aws_ami'):
- exit('aws_ami property must be set!')
- image_id = self.config.get('ec2', 'aws_ami')
- if not image_id:
- exit('aws_ami property was not properly')
-
- request['ImageId'] = image_id
- request['BlockDeviceMappings'] = get_block_device_map(instance_type)
-
- if self.config.has_option('ec2', 'key_name'):
- request['KeyName'] = self.config.get('ec2', 'key_name')
+ request['MinCount'] = 1
+ request['MaxCount'] = 1
tags = [{'Key': 'Name', 'Value': self.config.cluster_name + '-' + hostname},
{'Key': 'Muchos', 'Value': self.config.cluster_name}]
+
for key, val in self.config.instance_tags().items():
tags.append({'Key': key, 'Value': val})
+
request['TagSpecifications'] = [{'ResourceType': 'instance', 'Tags': tags}]
if self.config.has_option('ec2', 'user_data_path'):
@@ -85,7 +64,7 @@
if response is None or len(response['Instances']) != 1:
exit('ERROR - Failed to start {0} node'.format(hostname))
- print('Launching {0} node using {1}'.format(hostname, image_id))
+ print('Launching {0} node using {1}'.format(hostname, request['ImageId']))
return response['Instances'][0]
def create_security_group(self):
@@ -140,6 +119,38 @@
time.sleep(10)
print("Deleted security group")
+ def init_request(self, hostname, services, sg_id):
+ associate_public_ip = True
+ if self.config.has_option('ec2', 'associate_public_ip'):
+ associate_public_ip = self.config.get('ec2', 'associate_public_ip').strip().lower() == 'true'
+
+ request = {'NetworkInterfaces': [{'DeviceIndex': 0, 'AssociatePublicIpAddress': associate_public_ip,
+ 'Groups': [sg_id]}]}
+
+ if self.config.has_option('ec2', 'subnet_id'):
+ request['NetworkInterfaces'][0]['SubnetId'] = self.config.get('ec2', 'subnet_id')
+
+ if 'worker' in services:
+ instance_type = self.config.get('ec2', 'worker_instance_type')
+ else:
+ instance_type = self.config.get('ec2', 'default_instance_type')
+ request['InstanceType'] = instance_type
+ request['InstanceInitiatedShutdownBehavior'] = self.config.get('ec2', 'shutdown_behavior')
+
+ if not self.config.has_option('ec2', 'aws_ami'):
+ exit('aws_ami property must be set!')
+ image_id = self.config.get('ec2', 'aws_ami')
+ if not image_id:
+ exit('aws_ami property was not properly')
+
+ request['ImageId'] = image_id
+ request['BlockDeviceMappings'] = get_block_device_map(instance_type)
+
+ if self.config.has_option('ec2', 'key_name'):
+ request['KeyName'] = self.config.get('ec2', 'key_name')
+
+ return request
+
def launch(self):
if self.active_nodes():
exit('ERROR - There are already instances running for {0} cluster'.format(self.config.cluster_name))
@@ -227,3 +238,21 @@
print("Removed hosts file at ", self.config.hosts_path)
else:
print("Aborted termination")
+
+
+class Ec2ClusterTemplate(Ec2Cluster):
+
+ def __init__(self, config):
+ Ec2Cluster.__init__(self, config)
+
+ def launch(self):
+ print("Using cluster template '{0}' to launch nodes".format(self.config.cluster_template_d['id']))
+ super().launch()
+
+ def init_request(self, hostname, services, sg_id):
+ # the first service in the list denotes the node's target template
+ print("Template '{0}' selected for {1}".format(services[0], hostname))
+ # interpolate any values from the ec2 config section and create request
+ ec2_d = dict(self.config.items('ec2'))
+ ec2_d['security_group_id'] = sg_id
+ return json.loads(Template(self.config.cluster_template_d[services[0]]).substitute(ec2_d))
diff --git a/lib/tests/test_config.py b/lib/tests/test_config.py
index 99ac626..3d78275 100644
--- a/lib/tests/test_config.py
+++ b/lib/tests/test_config.py
@@ -20,7 +20,7 @@
def test_ec2_cluster():
c = DeployConfig("muchos", '../conf/muchos.props.example', '../conf/hosts/example/example_cluster',
- '../conf/checksums', 'mycluster')
+ '../conf/checksums', '../conf/templates', 'mycluster')
assert c.checksum_ver('accumulo', '1.9.0') == 'f68a6145029a9ea843b0305c90a7f5f0334d8a8ceeea94734267ec36421fe7fe'
assert c.checksum('accumulo') == 'df172111698c7a73aa031de09bd5589263a6b824482fbb9b4f0440a16602ed47'
assert c.get('ec2', 'default_instance_type') == 'm5d.large'
@@ -80,7 +80,7 @@
def test_existing_cluster():
c = DeployConfig("muchos", '../conf/muchos.props.example', '../conf/hosts/example/example_cluster',
- '../conf/checksums', 'mycluster')
+ '../conf/checksums', '../conf/templates', 'mycluster')
c.cluster_type = 'existing'
assert c.get_cluster_type() == 'existing'
assert c.node_type_map() == {}
@@ -95,9 +95,27 @@
def test_case_sensitive():
c = DeployConfig("muchos", '../conf/muchos.props.example', '../conf/hosts/example/example_cluster',
- '../conf/checksums', 'mycluster')
+ '../conf/checksums', '../conf/templates', 'mycluster')
assert c.has_option('ec2', 'default_instance_type') == True
assert c.has_option('ec2', 'Default_instance_type') == False
c.set('nodes', 'CamelCaseWorker', 'worker,fluo')
c.init_nodes()
assert c.get_node('CamelCaseWorker') == ['worker', 'fluo']
+
+
+def test_ec2_cluster_template():
+ c = DeployConfig("muchos", '../conf/muchos.props.example', '../conf/hosts/example/example_cluster',
+ '../conf/checksums', '../conf/templates', 'mycluster')
+
+ c.set('ec2', 'cluster_template', 'example')
+ c.init_template('../conf/templates')
+ # init_template already calls validate_template, so just ensure that
+ # we've loaded all the expected dictionary items from the example
+ assert 'accumulomaster' in c.cluster_template_d
+ assert 'client' in c.cluster_template_d
+ assert 'metrics' in c.cluster_template_d
+ assert 'namenode' in c.cluster_template_d
+ assert 'resourcemanager' in c.cluster_template_d
+ assert 'worker' in c.cluster_template_d
+ assert 'zookeeper' in c.cluster_template_d
+ assert 'devices' in c.cluster_template_d