b0VIM 7.4T2upeterlongnguyenPeters-MacBook-Pro.local~peterlongnguyen/Documents/work/pandacluster/src/pandacluster.coffeeutf-8 3210#"! Utpt[umv=q?$ /dad t^  L   u M L B   m *  | S H 3  r c b T   2 8#4sWCBm2n&dcUK # object of the AWS template. We wish to add additional strings to this array. # The cloud-config file is stored as an array of strings inside the "UserData"add_unit = (cloud_config, unit) -># Add unit to the cloud-config section of the AWS template. return build_error "Unable to access AWS template stores belonging to CoreOS", error catch error return template_object template_object = JSON.parse (yield get_body response) response = yield https_get template_url try template_url = template_store[channel][virtualization] template_store = parse( read( resolve( __dirname, "templates.cson"))) # This directory has a handy CSON file of URLs for CoreOS's latest CloudFormation templates. virtualization ||= "pv" channel ||= "stable" # Set reasonable defaults for these preferences.pull_cloud_template = async ({channel, virtualization}) -># Pulls the most recent AWS CloudFormation template from CoreOS.get_discovery_url = async -> yield get_body( yield https_get( "https://discovery.etcd.io/new"))# Wrapper for https call to etcd's discovery API. resolve error .on "error", (error) -> resolve data .on "end", -> data = data + chunk .on "data", (chunk) -> response.setEncoding "utf8" data = "" promise (resolve, reject) ->get_body = (response) -># Promise wrapper around response events that read "data" from the response's body. resolve error .on "error", (error) -> resolve response .on "response", (response) -> https.get url promise (resolve, reject) ->https_get = (url) -># Promise wrapper around Node's https module. Makes GET calls into promises. setTimeout callback, duration callback = -> resolve() promise (resolve, reject) ->pause = (duration) -># pause in execution for the specified duration (in ms).# This is a wrap of setTimeout with ES6 technology that forces a non-blocking node_lift method.bind objectlift_object = (object, method) -># Allow "when" to lift AWS module functions, which are non-standard. return build_error "ShellJS failed to execute shell command.", error() if error()? exec commandexecute = (command) -># Create a version of ShellJS's "exec" command with built-in error handling. } details: data if data? code: code if code? status: "success" message: message return {build_success = (message, data, code) -># Create a success object that reports data to user. return error error.details = details if details? error = new Error messagebuild_error = (message, details) -># Build an error object to let the user know something went worng.#================================# Helper Functions#================================mustache = require "mustache"# Template EngineAWS = require "aws-sdk"# Access AWS API{exec, error} = require "shelljs"# ShellJSasync = (require "when/generator").liftnode_lift = (require "when/node").lift{liftAll} = require "when/node"{promise, lift} = require "when"# When Library{where} = require "underscore"{parse} = require "c50n" # .cson file parsing{read, write} = require "fairmont" # Easy file read/write# In-House Libraries{resolve} = require "path"https = require "https"#====================# Modules#====================#===============================================================================# PandaCluster - Awesome Command-Line Tool and Library to Manage CoreOS Clusters#===============================================================================ad_ templatize: templatize return build_error "Apologies. The targeted cluster has not been destroyed.", errorad)A?i6YXW m _ ^ E ; f e W     k K    l R 1  A:9s%$#xEbDxw\K0lk catch error data, 200 return build_success "The targeted cluster has been destroyed. All related resources have been released.", destroy_cluster: yield destroy_cluster( params, credentials) data = # Gather data as we go. try # With everything in place, we may finally make a call to Amazon's API. #--------------------- # Access AWS #--------------------- params.StackName = options.stack_name params = {} # Build the "params" object that is used directly by the "createStack" method. credentials.region = options.region || credentials.region credentials = options.aws console.log "*****pandacluster destroy: ", options destroy: async (options) -> # This method stops and destroys a CoreOS cluster. return build_error "Apologies. The requested cluster cannot be created.", error console.log JSON.stringify error, null, '\t' catch error data, 201 return build_success "The requested cluster is online, configured, and ready.", console.log JSON.stringify data, null, '\t' data.customize_cluster = yield customize_cluster( options, credentials) # Continue setting up the cluster. options.ip_address = yield get_cluster_ip_address( options, credentials) # Now that the cluster is fully formed, grab its IP address for later. data.detect_formation = yield detect_formation( options, credentials) data.launch_stack = yield launch_stac cre credentials.region = options.region || credential console.log "*****credentials in pandacluster create: ", credentials credentials.region = options.region || credentials.region credentials = options.aws console.log "*****creat options: ", options create: async (options) -> # This method creates and starts a CoreOS cluster.module.exports =#===============================# PandaCluster Definition#=============================== throw build_error "Unable to access AWS Cloudformation.", err catch err return data data = yield delete_stack params try delete_stack = lift_object cf, cf.deleteStack cf = new AWS.CloudFormation() AWS.config = set_aws_creds credsdestroy_cluster = async (params, creds) -># Destroy a CoreOS cluster using the AWS account information that has been gathered. return build_error "Unable to properly configure cluster.", error catch error return build_success "Cluster customizations are complete.", data #data.launch_hook_server = yield launch_hook_server options #data.prepare_launch_repository = yield prepare_launch_repository options #data.launch_private_dns = yield launch_private_dns options, creds options.hostname = options.ip_address else yield pause 65000 # Wait 60s for the DNS records to finish updating. data.set_hostname = yield set_hostname options, creds if options.hostname? data = {} # Gather success data as we go. trycustomize_cluster = async (options, creds) -># into the cluster from a library of established unit-files and AWS commands.# After cluster formation is complete, launch a variety of services return build_error "Unable to install hook-server into cluster.", error catch error yield launch_service_unit "hook-server.service", config.hostname # Launch# after: config.after || "skydns.service"# # FIXME: how do i attach the "after" field?# ssh_keys: config.public_keys || []# templatize "services/hook-server.template", "src/services/hook-server.service", # FIXME: templatizead-[d3V-, `  B ( r L  S * l e d 6  dIb\&YSh87g@?dcE return build_error "Unable to build CloudFormation template.", error catch error return JSON.stringify template_object, null, "\t" # Return the JSON string. template_object.Resources.CoreOSServerLaunchConfig.Properties.UserData = user_data user_data["Fn::Base64"]["Fn::Join"][1] = cloud_config # Place this array back into the JSON object. Construction complete. cloud_config.push " - #{x}\n" for x in options.public_keys cloud_config.push "ssh_authorized_keys: \n" unless options.public_keys == [] # Add the specified public keys. We must be careful with indentation formatting. cloud_config = add_unit cloud_config, x for x in options.formation_units unless !options.formation_units || options.formation_units == [] #prepare_docker_mount_template options.ephemeral_drive #prepare_ephemeral_drive_template options.ephemeral_drive # Defaults to creating a service file with drive /dev/xvdb. # Creates .service and .mount files from templates, even if files not used. # Add the specified units to the cloud-config section. cloud_config = user_data["Fn::Base64"]["Fn::Join"][1] user_data = template_object.Resources.CoreOSServerLaunchConfig.Properties.UserData # Isolate the cloud-config array within the JSON object. template_object = yield pull_cloud_template options # Pull official CoreOS template as a JSON object. trybuild_template = async (options) -># by CoreOS. Return a JSON string.# Build an AWS CloudFormation template by augmenting the official ones released true # FIXME: return properly , will need async and lift for templatizing drive_path: drive_path templatize "services/var-lib-docker.template", "src/services/var-lib-docker.mount", drive_path = drive_path || "/dev/xvdb"prepare_docker_mount_template = (drive_path) -># Defaults drive path to /dev/xvdb# Creates var-lib-docker.mount from template. true # FIXME: return properly , will need async and lift for templatizing drive_path: drive_path templatize "services/format-ephemeral.template", "src/services/format-ephemeral.service", drive_path = drive_path || "/dev/xvdb"prepare_ephemeral_drive_template = (drive_path) -># Defaults drive path to /dev/xvdb# Creates format-ephemeral.service from template. true write resolve(relative_write_path), results_text # FIXME: need to use async and lift file read/write # Write interpolated string to file results_text = mustache.render hook_server_template.toString(), data # Turn file into interpolated string hook_server_template = read( resolve( __dirname, relative_read_path)) # Read in template filetemplatize = (relative_read_path, relative_write_path, data) -> return cloud_config content.shift() cloud_config.push " " + content[0] + "\n" while content.length > 0 content = content.split "\n" content = read( resolve( __dirname, "services/#{unit.name}")) # line with an explicit new-line character. # eight spaces to the begining of each line (4 indentations) and follow each # For "content", we draw from a unit-file maintained in a separate file. Add cloud_config.push " content: |\n" cloud_config.push " enable: #{unit.enable}\n" if unit.enable? cloud_config.push " command: #{unit.command}\n" if unit.command? cloud_config.push " runtime: #{unit.runtime}\n" if unit.runtime? cloud_config.push " - name: #{unit.name}\n" # Add to the cloud_config array. # which is sensitive to indentation.... # We need to be careful because "cloud-config" files are formatted in YAML,admrYD@?>\[U. n m Q P D   | > .  o K J Z q i h R , s?76$JQP+F}BA;,+xwk('&# Cluster creation can take several minutes. This function polls AWS until the return build_error "Unable to access AWS CloudFormation.", err catch err return false else return build_error "AWS CloudFormation returned status \"CREATE_FAILED\".", data else if data.StackEvents.ResourceStatus == "CREATE_FAILED" return build_success "The cluster is confirmed to be online and ready.", data data.StackEvents[0].ResourceStatus == "CREATE_COMPLETE" if data.StackEvents[0].ResourceType == "AWS::CloudFormation::Stack" && data = yield describe_events {StackName: name} try describe_events = lift_object cf, cf.describeStackEvents cf = new AWS.CloudFormation() AWS.config = set_aws_creds credsget_formation_status = async (name, creds) -># It returns either true or false, and throws an exception if an AWS error is reported.# This function checks the specified AWS stack to see if its formation is complete. return build_error "Unable to access AWS CloudFormation", err catch err return build_success "Cluster formation in progress.", data data = yield create_stack params create_stack = lift_object cf, cf.createStack cf = new AWS.CloudFormation() AWS.config = set_aws_creds creds # Preparations complete. Access AWS. ] # AllowSSHFrom - uses default "everywhere", TODO: Add this option # AdvertisedIPAddress - uses default "private", TODO: Add this option } "ParameterValue": options.key_pair if yield validate_key_pair( options.key_pair, creds) "ParameterKey": "KeyPair" { # KeyPair } "ParameterValue": yield get_discovery_url() "ParameterKey": "DiscoveryURL" { # DiscoveryURL - Grab a randomized URL from etcd's free discovery service. } "ParameterValue": options.cluster_size || "3" "ParameterKey": "ClusterSize" { # ClusterSize } "ParameterValue": options.instance_type || "m3.medium" "ParameterKey": "InstanceType" { # InstanceType params.Parameters = [ #--------------------------------------------------------------------------- # template file. We will now fill out the map as specified or with defaults. # Parameters is a map of key/values custom defined for this stack by the #--------------------------------------------------------------------------- console.log params.TemplateBody #params.TemplateBody = JSON.stringify params.TemplateBody params.TemplateBody = yield build_template options params.OnFailure = "DELETE" params.StackName = options.stack_name params = {} # Build the "params" object that is used directly by AWS. trylaunch_stack = async (options, creds) -># Launch the procces that eventually creates a CoreOS cluster using the user's AWS account. return build_error "Unable to validate SSH key.", err catch err return true # validated return build_error "This AWS account does not have a key pair named \"#{key_pair}\"." unless key_pair in names names.push key.KeyName for key in data.KeyPairs names = [] data = yield describe_key_pairs {} try describe_key_pairs = lift_object ec2, ec2.describeKeyPairs ec2 = new AWS.EC2() AWS.config = set_aws_creds credsvalidate_key_pair = async (key_pair, creds) -># Confirm that the named SSH key exists in your AWS account. } sslEnabled: true region: creds.region secretAccessKey: creds.key accessKeyId: creds.id return {set_aws_creds = (creds) -># Configure the AWS object for account access.ad)vu0 PON3 O N A 0 & w m c = ) ^ ] M    g a   c b YPA@? h21s4321q21g98*pj # Construct the "params" object that is used directly by the AWS method. tryset_hostname = async (options, creds) -># Associate the cluster's IP address with a domain the user owns via Route 53. return build_error "Unable to access AWS Route 53.", error catch error return record[0].ResourceRecords[0].Value record = where data.ResourceRecordSets, {Name:hostname} # We need to conduct a little parsing to extract the IP address of the record set. data = yield list_records {HostedZoneId: zone_id} list_records = lift_object r53, r53.listResourceRecordSets r53 = new AWS.Route53() AWS.config = set_aws_creds creds tryget_record_ip_address = async (hostname, zone_id, creds) -># Get the IP address currently associated with the hostname. return build_error "Unable to access AWS Route 53.", error catch error return where( data.HostedZones, {Name:root_domain})[0].Id # Dig the ID out of an array, holding an object, holding the string we need. data = yield list_zones {} list_zones = lift_object r53, r53.listHostedZones r53 = new AWS.Route53() AWS.config = set_aws_creds creds root_domain = get_root_domain hostname tryget_hosted_zone_id = async (hostname, creds) -># Get the AWS HostedZoneID for the specified domain. return build_error "There was an issue parsing the requested hostname.", error catch error return domain domain = domain + "." # And finally, make the sure the root_domain ends with a "." domain = "#{foo[foo.length - 2]}.#{foo[foo.length - 1]}" else domain = "#{foo[foo.length - 3]}.#{foo[foo.length - 2]}" if foo[foo.length - 1] == "" foo = domain.split "." # Be careful of tld's that are followed by a period. # Now grab the root domain, the top-level-domain, plus what's to the left of it. domain = domain.split(':')[0] # Find and remove port number domain = url.split('/')[0] else domain = url.split('/')[2] if url.indexOf("://") != -1 # Find and remove protocol (http, ftp, etc.), if present, and get domain tryget_root_domain = (url) -># https://awesome.example.com/test/42#?=What+is+the+answer => example.com.# Given a URL of many possible formats, return the root domain. return build_error "Unable to access AWS EC2.", error catch error return data.Reservations[0].Instances[0].PublicIpAddress data = yield describe_instances params try ] } ] "16" # Only examine running instances. Values: [ Name: "instance-state-code" { } ] options.stack_name # Only examine instances within the stack we just created. Values: [ Name: "tag:aws:cloudformation:stack-name" { Filters: [ params = describe_instances = lift_object ec2, ec2.describeInstances ec2 = new AWS.EC2() AWS.config = set_aws_creds credsget_cluster_ip_address = async (options, creds) ->#-------------------------# Cluster Customization#------------------------- return build_error "Unable to detect cluster formation.", error catch error yield pause 5000 # Not complete, keep going. else return status # The cluster formation complete. if status status = yield get_formation_status(options.stack_name, creds) while true trydetect_formation = async (options, creds) -># CoreOS cluster is fully formed and ready for additional instructions.adqkjRE) { d C 1 l S <  ] A h g Y h : 4 3 % },tYML8H{zaS+a>h!tMG # Add public SSH keys. # Customize the hook-server unit file template. trylaunch_hook_server = async (config) -># and launch githook scripts. The hook-server is loaded with all cluster public keys.# Place a hook-server on the cluster that will respond to users' git commands true # FIXME: return properly , will need async and lift for templatizing drive_path: drive_path templatize "services/format-ephemeral.template", "src/services/format-ephemeral.service", drive_path = drive_path || "/dev/xvdb"prepare_hook_server_template = ({ssh_keys, after}) -># Defaults drive path to /dev/xvdb# Creates hook-server.service from template. return build_error "Unable to launch service unit.", error catch error execute command "EOF" "fleetctl start services/#{name}\n" + "ssh core@#{hostname} << EOF\n" + command = # Launch the service execute "scp #{__dirname}/services/#{name} core@#{hostname}:/home/core/services/." # Place a copy of the customized unit file on the cluster. trylaunch_service_unit = async (name, hostname) -># Helper function that launches a single unit from PandaCluster's library onto the cluster. return build_error "Unable to install the Launch Repository.", error catch error return build_success "The Launch Repository is ready." execute command "EOF" "mkdir services\n" + "ssh core@#{config.hostname} << EOF\n" + command = tryprepare_launch_repository = async (config) -># containing a Dockerfile, *.service file, and anything else it needs.# launch acts as a repository where each service will have its own sub-directory# Prepare the cluster to accept services by installing a directory called "launch". return build_error "Unable to establish the cluster's private DNS.", error catch error trylaunch_private_dns = async (domain, creds) -># so services may be referenced with human-friendly names.# Using Private DNS from Route 53, we need to give the cluster a private DNS return build_error "Unable to assign the cluster's IP address to the designated hostname.", error catch error return build_success "The domain \"#{options.hostname}\" has been assigned to #{options.ip_address}.", data data = yield change_record params change_record = lift_object r53, r53.changeResourceRecordSets r53 = new AWS.Route53() AWS.config = set_aws_creds creds # We are ready to access AWS. ] } ] } Value: options.ip_address { ResourceRecords: [ TTL: 60, Type: "A", Name: options.hostname, ResourceRecordSet: Action: "CREATE", { } ] } Value: old_ip_address { ResourceRecords: [ TTL: 60, Type: "A", Name: options.hostname, ResourceRecordSet: Action: "DELETE", { Changes: [ ChangeBatch: HostedZoneId: zone_id params = # will need to the "AliasTarget" sub-object here. # TODO: When we establish an Elastic Load Balancer solution, we # records. Here we delete the old record and add the new IP address. # The params object contains "Changes", an array of actions to take on the DNS old_ip_address = yield get_record_ip_address( options.hostname, zone_id, creds) zone_id = yield get_hosted_zone_id( options.hostname, creds)ad /iV ~ l k [ ( | E ' [ Z ? .  O N } cat catch error data, 200 return build_success "T catch error data, 200 return build_success "The targeted cluster has been destroyed. All related resources have been released.", destroy_cluster: yield destroy_cluster( params, credentials) data = # Gather data as we go. try # With everything in place, we may finally make a call to Amazon's API. #--------------------- # Access AWS #--------------------- params.StackName = options.stack_name params = {} # Build the "params" object that is used directly by the "createStack" method. credentials.region = options.region || credentials.region credentials = options.aws console.log "*****pandacluster destroy: ", options destroy: async (options) -> # This method stops and destroys a CoreOS cluster. return build_error "Apologies. The requested cluster cannot be created.", error console.log JSON.stringify error, null, '\t' catch error data, 201 return build_success "The requested cluster is online, configured, and ready.", console.log JSON.stringify data, null, '\t' data.customize_cluster = yield customize_cluster( options, credentials) # Continue setting up the cluster. options.ip_address = yield get_cluster_ip_address( options, credentials) # Now that the cluster is fully formed, grab its IP address for later. data.detect_formation = yield detect_formation( options, credentials) data.launch_stack = yield launch_stack( options, credentials) data = {} # Make calls to Amazon's API. Gather data as we go. try