UNPKG

vortex

Version:
517 lines (404 loc) 15.8 kB
fs = require 'fs' async = require 'async' aws_sdk = require 'aws-sdk' logsmith = require 'logsmith' path_extra = require 'path-extra' portchecker = require 'portchecker' # --- exports.Provider = class ### This class exposes Amazon as a provider to Vortex. ### constructor: (@manifest) -> ### The provider accepts a manifest as a parameter by specification. ### aws_sdk.config.update @extract_client_options() get_node: (node_name) -> ### This method returns a node by looking up its name. It throws an error if the node is not found. ### return @manifest.nodes[node_name] if @manifest.nodes? and @manifest.nodes[node_name]? throw new Error "node #{node_name} does not exist" extract_property: (property_name, node_name) -> ### Extracts a property by looking into a node and upper layers of the manifest. ### try node = @get_node node_name catch e node = null return node.amazon[property_name] if node?.amazon?[property_name]? return @manifest.amazon[property_name] if @manifest.amazon?[property_name]? return null # # Helper functions for extracting various properties. # extract_access_key_id: (node_name) -> @extract_property 'accessKeyId', node_name extract_secret_access_key: (node_name) -> @extract_property 'secretAccessKey', node_name extract_region: (node_name) -> @extract_property 'region', node_name extract_max_retries: (node_name) -> @extract_property 'maxRetries', node_name extract_image_id: (node_name) -> @extract_property 'imageId', node_name extract_instance_type: (node_name) -> @extract_property 'instanceType', node_name extract_key_name: (node_name) -> @extract_property 'keyName', node_name extract_security_groups: (node_name) -> @extract_property 'securityGroups', node_name extract_user_data: (node_name) -> @extract_property 'userData', node_name extract_disable_api_termination: (node_name) -> @extract_property 'disableApiTermination', node_name extract_username: (node_name) -> @extract_property 'username', node_name extract_password: (node_name) -> @extract_property 'password', node_name extract_private_key: (node_name) -> @extract_property 'privateKey', node_name extract_passphrase: (node_name) -> @extract_property 'passphrase', node_name extract_ssh_port: (node_name) -> @extract_property 'sshPort', node_name # # # extract_namespace: (node_name) -> ### Extracts a namespace by looking it up in the node itself and upper layers of the manifest ### try node = @get_node node_name catch node = null return node.namespace if node?.namespace? return @manifest.namespace if @manifest.namespace? extract_client_options: (node_name) -> ### Extracts options related to the AWS client. ### access_key_id = @extract_access_key_id node_name secret_access_key = @extract_secret_access_key node_name region = @extract_region node_name max_retries = @extract_max_retries node_name options = {} options.accessKeyId = access_key_id if access_key_id options.secretAccessKey = secret_access_key if secret_access_key options.region = region if region options.maxRetries = max_retries if max_retries return options extract_instance_options: (node_name) -> ### Extracts options related to AWS instances. ### image_id = this.extract_image_id node_name instance_type = this.extract_instance_type node_name key_name = this.extract_key_name node_name security_groups = this.extract_security_groups node_name user_data = this.extract_user_data node_name disable_api_termination = this.extract_disable_api_termination node_name options = {} options.ImageId = image_id if image_id options.InstanceType = instance_type if instance_type options.KeyName = key_name if key_name options.SecurityGroups = security_groups if security_groups options.UserData = user_data if user_data options.DisableApiTermination = disable_api_termination if disable_api_termination return options get_client: (node_name) -> ### Obtain a client for EC2. ### return new aws_sdk.EC2 @extract_client_options node_name create_error: (error, node_name) -> ### Creates a friendlier error message. ### if error.code == 'NetworkingError' return error else tokens = error.toString().split(':') type = tokens[0] message = tokens[1].trim() parts = message.split('.') message = parts.shift().toLowerCase().trim() if node_name message = "#{message} for node #{node_name}" if parts.length > 0 message = "#{message} (#{parts.join('.').trim()})" message = message.replace /\s'(\w+)'\s/, (match, group) -> param = group.toLowerCase() switch param when 'accesskeyid' then param = 'accessKeyId' when 'secretaccesskey' then param = 'secretAccessKey' when 'region' then param = 'region' when 'maxretries' then param = 'maxRetries' when 'imageid' then param = 'imageId' when 'instancetype' then param = 'instanceType' when 'keyname' then param = 'keyName' when 'securitygroups' then param = 'securityGroups' when 'userdata' then param = 'userData' when 'disableapitermination' then param = 'disableApiTermination' return ' "' + param + '" ' message = message[0] + message.substring 1, message.length return new Error message bootstrap: (node_name, callback) -> ### Provider-specific method for bootstrapping a node. ### # # First we verify the status of the node to check if the state is correct. # verify_status = (callback) => @status node_name, (err, state, address) -> return callback err if err return callback new Error "node #{node_name} is not ready" if state != 'running' return callback null # # Next we obtain shell spec. # obtain_shell_spec = (callback) => @shell_spec node_name, (err, spec) -> return callback err if err return callback null, spec # # Next we check the exposed files and folders. # prepare_exposed = (spec, callback) => try node = @get_node node_name catch e node = null return callback null if not node?.expose? handle_exposure = (exposure, callback) => source_path = path_extra.resolve path_extra.dirname(@manifest.meta.location), exposure.src fs.stat source_path, (err, stats) => return callback new Error "cannot expose #{exposure.src} because it does not exist" if err return callback null async.eachSeries ({src: src, dst: dst} for src, dst of node.expose), handle_exposure, callback # # Action on the tasks. # async.waterfall [verify_status, obtain_shell_spec, prepare_exposed], (err, state, address) -> return callback err if err return callback null status: (node_name, callback) -> ### Provider-specific method for checking the status of a node. ### try client = @get_client node_name catch e return callback @create_error e, node_name options = Filters: [ {Name: 'tag:vortex-node-name', Values: [node_name]} {Name: 'tag:vortex-node-namespace', Values: [this.extract_namespace(node_name)]} ] logsmith.debug 'describe instances with options', options client.describeInstances options, (err, result) => return callback @create_error err, node_name if err instances = [] for reservation in result.Reservations for instance in reservation.Instances instances.push { id: instance.InstanceId state: instance.State.Name address: instance.PublicDnsName } return callback null, 'stopped' if instances.length == 0 logsmith.debug 'discovered instances', instances selected_instance = instances[instances.length - 1] return callback new Error "could not obtain instance for node #{node_name}" if not selected_instance logsmith.debug 'selected instance', selected_instance for instance in instances if instance.state not in ['shutting-down', 'terminated', 'stopping', 'stopped'] and selected_instance != instance logsmith.warn "duplicate node #{node_name} with instance id #{instance.id} detected" state = switch selected_instance.state when 'pending' then 'booting' when 'running' then 'running' when 'stopped' then 'stopped' when 'stopping' then 'halting' when 'terminated' then 'stopped' when 'shutting-down' then 'halting' else null return callback new Error "undefined state for node #{node_name}" if not state logsmith.debug "node #{node_name} with instance id #{selected_instance.id} has state #{state}" address = selected_instance.address if not address state = 'booting' if state != 'running' address = null return callback null, state, address, selected_instance.id boot: (node_name, callback) -> ### Provider-specific method for booting a node. ### try client = @get_client node_name catch e return callback @create_error e, node_name # # First we verify the status of the node to check if the state is correct. # verify_status = (callback) => @status node_name, (err, state, address) -> return callback err if err return callback new Error "node #{node_name} is already booting" if state == 'booting' return callback new Error "node #{node_name} is already running" if state == 'running' return callback new Error "node #{node_name} is halting" if state == 'halting' return callback null # # Next we run the instance. # run_instance = (callback) => options = @extract_instance_options node_name options.MinCount = 1 options.MaxCount = 1 logsmith.debug 'run instances with options', options client.runInstances options, (err, result) => return callback @create_error err, node_name if err instances = [] for instance in result.Instances instances.push { id: instance.InstanceId } return callback new Error "no instances run for node #{node_name}" if instances.length == 0 logsmith.debug 'ran instances', instances selected_instance = instances[instances.length - 1] return callback new Error "could not create instance for node #{node_name}" if not selected_instance logsmith.debug 'selected instance', selected_instance for instance in instances if selected_instance != instance logsmith.warn "duplicate node #{node_name} with instance id #{instance_id} detected" return callback null, selected_instance.id # # Finally we unmap any tags on the instance. # map_tags = (instance_id, callback) => options = Resources: [instance_id] Tags: [ {Key: 'vortex-node-name', Value: node_name} {Key: 'vortex-node-namespace', Value: @extract_namespace node_name} ] logsmith.debug 'create tags with options', options client.createTags options, (err, result) => return callback @create_error err, node_name if err return callback null, instance_id # # Action on the tasks. # async.waterfall [verify_status, run_instance, map_tags], (err) => return callback err if err return @status node_name, callback halt: (node_name, callback) -> ### Provider-specific method for halting a node. ### try client = @get_client node_name catch e return callback @create_error e, node_name # # First we verify the status of the node to check if the state is correct. # verify_status = (callback) => @status node_name, (err, state, address, instance_id) -> return callback err if err return callback new Error "#{node_name} is already halting" if state == 'halting' return callback new Error "#{node_name} is already stopped" if state == 'stopped' return callback null, instance_id # # Next we terminate the instance. # terminate_instance = (instance_id, callback) => options = InstanceIds: [instance_id] logsmith.debug 'terminate instances with options', options client.terminateInstances options, (err, result) => return callback @create_error err, node_name if err return callback null, instance_id # # Finally we unmap any tags on the instance. # unmap_tags = (instance_id, callback) => options = Resources: [instance_id] Tags: [ {Key: 'vortex-node-name', Value: node_name} {Key: 'vortex-node-namespace', Value: @extract_namespace node_name} ] logsmith.debug 'delete tags with options', options client.deleteTags options, (err, result) => return callback @create_error err, node_name if err return callback null, instance_id # # Action on the tasks. # async.waterfall [verify_status, terminate_instance, unmap_tags], (err) => return callback err if err return @status node_name, callback pause: (node_name, callback) -> ### Provider-specific method for pausing a machine. ### return callback new Error "cannot pause node #{node_name} due to pause not implemented" resume: (node_name, callback) -> ### Provider-specific method for resuming a machine. ### return callback new Error "cannot resume node #{node_name} due to resume not implemented" shell_spec: (node_name, callback) -> ### Provider-specific method for obtaining a shell spec from a node. ### password = @extract_password node_name private_key = @extract_private_key node_name return callback new Error "no password or privateKey provided for node #{node_name}" if not password and not private_key ssh_port = @extract_ssh_port node_name if ssh_port ssh_port = parseInt ssh_port, 10 return callback new Error "ssh port for node #{node_name} is incorrect" if isNaN ssh_port or ssh_port < 1 else ssh_port = 22 username = @extract_username node_name if not username username = 'vortex' passphrase = @extract_passphrase node_name # # First we obtain the node status by looking for the address and to check if the state is correct. # obtain_status = (callback) => @status node_name, (err, state, address) -> return callback err if err return callback new Error "node #{node_name} is halting" if state == 'halting' return callback new Error "node #{node_name} is stopped" if state == 'stopped' return callback new Error "cannot find network address for node #{node_name}" if not address return callback null, address # # Next we continiusly check if the ssh port is open. # ensure_port = (address, callback) -> portchecker.isOpen ssh_port, address, (is_open) -> return callback null, address if is_open callee = arguments.callee milliseconds = 10000 timeout = () -> portchecker.isOpen ssh_port, address, callee logsmith.debug "repeat check for ssh port open for node #{node_name} in #{milliseconds} milliseconds" setTimeout timeout, milliseconds # # Finally we build the spec and send it off. # build_spec = (address, callback) -> parts = [] parts.push 'ssh://' parts.push encodeURIComponent username parts.push ':' + encodeURIComponent password if password parts.push '@' parts.push address parts.push ':' + ssh_port parts.push ';privateKey=' + encodeURIComponent private_key if private_key parts.push ';passphrase=' + encodeURIComponent passphrase if passphrase spec = parts.join '' spec_options = username: username password: password host: address port: ssh_port privateKey: private_key passphrase: passphrase return callback null, spec, spec_options # # Action on the tasks. # async.waterfall [obtain_status, ensure_port, build_spec], callback