linecook-gem 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/linecook/builder/build.rb +28 -0
- data/lib/linecook/{darwin_backend.rb → builder/darwin_backend.rb} +4 -1
- data/lib/linecook/{linux_backend.rb → builder/linux_backend.rb} +3 -3
- data/lib/linecook/{lxc.rb → builder/lxc.rb} +25 -43
- data/lib/linecook/builder/manager.rb +56 -0
- data/lib/linecook/cli.rb +77 -12
- data/lib/linecook/image/crypt.rb +49 -0
- data/lib/linecook/image/github.rb +27 -0
- data/lib/linecook/image/manager.rb +43 -0
- data/lib/linecook/image/s3.rb +47 -0
- data/lib/linecook/packager/ebs.rb +270 -0
- data/lib/linecook/packager/manager.rb +23 -0
- data/lib/linecook/{chef.rb → provisioner/chef-zero.rb} +3 -2
- data/lib/linecook/provisioner/manager.rb +34 -0
- data/lib/linecook/provisioner/packer.rb +82 -0
- data/lib/linecook/{config.rb → util/config.rb} +44 -4
- data/lib/linecook/util/downloader.rb +35 -0
- data/lib/linecook/util/executor.rb +33 -0
- data/lib/linecook/{ssh.rb → util/ssh.rb} +59 -2
- data/lib/linecook/version.rb +1 -1
- data/lib/linecook.rb +5 -4
- metadata +76 -11
- data/lib/linecook/bake.rb +0 -21
- data/lib/linecook/build.rb +0 -21
- data/lib/linecook/builder.rb +0 -93
- data/lib/linecook/image.rb +0 -51
| @@ -0,0 +1,270 @@ | |
| 1 | 
            +
            require 'net/http'
         | 
| 2 | 
            +
            require 'open-uri'
         | 
| 3 | 
            +
            require 'timeout'
         | 
| 4 | 
            +
            require 'tmpdir'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'linecook/util/ssh'
         | 
| 7 | 
            +
            require 'linecook/util/executor'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require 'aws-sdk'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            module Linecook
         | 
| 12 | 
            +
              # Installs a linecook image on a target device
         | 
| 13 | 
            +
              module Packager
         | 
| 14 | 
            +
                class EBS
         | 
| 15 | 
            +
                  include Executor
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize(hvm: true, size: 10, region: 'us-east-1')
         | 
| 18 | 
            +
                    @hvm = hvm
         | 
| 19 | 
            +
                    @size = size
         | 
| 20 | 
            +
                    @region = region
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def package(image)
         | 
| 24 | 
            +
                    @image = image
         | 
| 25 | 
            +
                    setup_remote unless instance_id
         | 
| 26 | 
            +
                    prepare
         | 
| 27 | 
            +
                    execute("tar -C #{@mountpoint} -cpf - . | sudo tar -C #{@root} -xpf -")
         | 
| 28 | 
            +
                    finalize
         | 
| 29 | 
            +
                    snapshot
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                private
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def prepare
         | 
| 35 | 
            +
                    @mountpoint = Dir.mktmpdir
         | 
| 36 | 
            +
                    execute("mkdir -p #{@mountpoint}")
         | 
| 37 | 
            +
                    execute("mount #{@image} #{@mountpoint}")
         | 
| 38 | 
            +
                    create_volume
         | 
| 39 | 
            +
                    format_and_mount
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def finalize
         | 
| 43 | 
            +
                    execute("echo \"UUID=\\\"$(blkid -o value -s UUID #{@rootdev})\\\" /               ext4            defaults  1 2\" > /tmp/fstab")
         | 
| 44 | 
            +
                    execute("mv /tmp/fstab #{@root}/etc/fstab")
         | 
| 45 | 
            +
                    chroot_exec('apt-get update')
         | 
| 46 | 
            +
                    chroot_exec('apt-get install -y --force-yes grub-pc grub-legacy-ec2')
         | 
| 47 | 
            +
                    chroot_exec('update-grub')
         | 
| 48 | 
            +
                    execute("grub-install --root-directory=#{@root} $(echo #{@rootdev} | sed \"s/[0-9]*//g\")") if @hvm
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def chroot_exec(command)
         | 
| 53 | 
            +
                    execute("mount -o bind /dev #{@root}/dev")
         | 
| 54 | 
            +
                    execute("mount -o bind /sys #{@root}/sys")
         | 
| 55 | 
            +
                    execute("mount -t proc none #{@root}/proc")
         | 
| 56 | 
            +
                    execute("cp /etc/resolv.conf #{@root}/etc")
         | 
| 57 | 
            +
                    execute("chroot #{@root} #{command}")
         | 
| 58 | 
            +
                    execute("umount #{@root}/dev")
         | 
| 59 | 
            +
                    execute("umount #{@root}/sys")
         | 
| 60 | 
            +
                    execute("umount #{@root}/proc")
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def partition
         | 
| 64 | 
            +
                    execute("parted -s #{@rootdev} mklabel msdos")
         | 
| 65 | 
            +
                    execute("parted -s #{@rootdev} mkpart primary ext2 0% 100%")
         | 
| 66 | 
            +
                    @rootdev = "#{@rootdev}1"
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def format_and_mount
         | 
| 70 | 
            +
                    partition if @hvm
         | 
| 71 | 
            +
                    execute("mkfs.ext4 #{@rootdev}")
         | 
| 72 | 
            +
                    @root = Dir.mktmpdir
         | 
| 73 | 
            +
                    execute("mkdir -p #{@root}")
         | 
| 74 | 
            +
                    execute("mount #{@rootdev} #{@root}")
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def instance_id
         | 
| 78 | 
            +
                    @instance_id ||= metadata('instance-id')
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def availability_zone
         | 
| 82 | 
            +
                    @availability_zone ||= metadata('placement/availability-zone')
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  def client
         | 
| 86 | 
            +
                    @client ||= begin
         | 
| 87 | 
            +
                      credentials = Aws::Credentials.new(Linecook::Config.secrets['aws_access_key'], Linecook::Config.secrets['aws_secret_key'])
         | 
| 88 | 
            +
                      Aws::EC2::Client.new(region: @region, credentials: credentials)
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def create_volume
         | 
| 93 | 
            +
                    resp = client.create_volume({
         | 
| 94 | 
            +
                      size: @size,
         | 
| 95 | 
            +
                      availability_zone: availability_zone, # required
         | 
| 96 | 
            +
                      volume_type: "standard", # accepts standard, io1, gp2
         | 
| 97 | 
            +
                    })
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    @volume_id = resp.volume_id
         | 
| 100 | 
            +
                    rootdev = free_device
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    puts "Waiting for volume to become available"
         | 
| 103 | 
            +
                    wait_for_state('available', 120) do
         | 
| 104 | 
            +
                      client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    resp = client.attach_volume({
         | 
| 108 | 
            +
                      volume_id: @volume_id,
         | 
| 109 | 
            +
                      instance_id: instance_id,
         | 
| 110 | 
            +
                      device: rootdev,
         | 
| 111 | 
            +
                    })
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    puts "Waiting for volume to attach"
         | 
| 114 | 
            +
                    wait_for_state('attached', 120) do
         | 
| 115 | 
            +
                      client.describe_volumes(volume_ids: [@volume_id]).volumes.first.attachments.first.state
         | 
| 116 | 
            +
                    end
         | 
| 117 | 
            +
                    @rootdev = "/dev/#{rootdev}"
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def snapshot
         | 
| 121 | 
            +
                    execute("umount #{@root}")
         | 
| 122 | 
            +
                    puts 'Creating snapshot'
         | 
| 123 | 
            +
                    resp = client.detach_volume(volume_id: @volume_id)
         | 
| 124 | 
            +
                    wait_for_state('available', 120) do
         | 
| 125 | 
            +
                      client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                    resp = client.create_snapshot(volume_id: @volume_id, description: "Snapshot of #{File.basename(@image)}")
         | 
| 128 | 
            +
                    tag(resp.snapshot_id, Name: 'Linecook snapshot', image: File.basename(@image), hvm: @hvm.to_s)
         | 
| 129 | 
            +
                    client.delete_volume(volume_id: @volume_id)
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  def free_device
         | 
| 133 | 
            +
                    prefix = device_prefix
         | 
| 134 | 
            +
                    ('f'..'zzz').to_a.each do |suffix|
         | 
| 135 | 
            +
                      device = "#{prefix}#{suffix}"
         | 
| 136 | 
            +
                      if free_device?(device)
         | 
| 137 | 
            +
                        lock_device(device)
         | 
| 138 | 
            +
                        return device
         | 
| 139 | 
            +
                      end
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
                    return nil
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  def free_device?(device)
         | 
| 145 | 
            +
                    test("[ ! -e /dev/#{device} ]") && test("[ ! -e /run/lock/linecook-#{device} ]")
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  def lock_device(device)
         | 
| 149 | 
            +
                    execute("echo #{Process.pid} > /run/lock/linecook-#{device}")
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                  def unlock_device(device)
         | 
| 153 | 
            +
                    execute("rm /run/lock/linecook-#{device}")
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def device_prefix
         | 
| 157 | 
            +
                    prefixes = ['xvd', 'sd']
         | 
| 158 | 
            +
                    capture('ls -1 /sys/block').lines.each do |dev|
         | 
| 159 | 
            +
                      prefixes.each do |prefix|
         | 
| 160 | 
            +
                        return prefix if dev =~ /^#{prefix}/
         | 
| 161 | 
            +
                      end
         | 
| 162 | 
            +
                    end
         | 
| 163 | 
            +
                    return nil
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  def setup_remote
         | 
| 167 | 
            +
                    start_node
         | 
| 168 | 
            +
                    path = "/tmp/#{File.basename(@image)}"
         | 
| 169 | 
            +
                    @remote.run("wget '#{Linecook::ImageManager.url(File.basename(@image))}' -nv -O #{path}")
         | 
| 170 | 
            +
                    @image = Linecook::Crypto.new(remote: @remote).decrypt_file(path)
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  def start_node
         | 
| 174 | 
            +
                    resp = client.run_instances(
         | 
| 175 | 
            +
                      image_id: find_ami,
         | 
| 176 | 
            +
                      min_count: 1,
         | 
| 177 | 
            +
                      max_count: 1,
         | 
| 178 | 
            +
                      instance_type: 'c4.large',
         | 
| 179 | 
            +
                      instance_initiated_shutdown_behavior: 'terminate',
         | 
| 180 | 
            +
                      security_groups: [security_group],
         | 
| 181 | 
            +
                      key_name: key_pair
         | 
| 182 | 
            +
                    )
         | 
| 183 | 
            +
                    @instance_id = resp.instances.first.instance_id
         | 
| 184 | 
            +
                    @availability_zone = resp.instances.first.placement.availability_zone
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                    puts 'Waiting for temporary instance to come online'
         | 
| 187 | 
            +
                    wait_for_state('running', 300) do
         | 
| 188 | 
            +
                      client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.state.name
         | 
| 189 | 
            +
                    end
         | 
| 190 | 
            +
                    tag(@instance_id, Name: 'linecook-temporary-installer-node')
         | 
| 191 | 
            +
                    @remote = Linecook::SSH.new(instance_ip, username: 'ubuntu', keyfile: Linecook::SSH.private_key)
         | 
| 192 | 
            +
                    @remote.upload("exec shutdown -h 60 'Delayed shutdown started'", '/tmp/delay-shutdown')
         | 
| 193 | 
            +
                    execute('mv /tmp/delay-shutdown /etc/init/delay-shutdown.conf') # ubuntism is ok, since the temporary host can always be ubuntu
         | 
| 194 | 
            +
                    execute('start delay-shutdown')
         | 
| 195 | 
            +
                  end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  def find_ami
         | 
| 198 | 
            +
                    url = "http://uec-images.ubuntu.com/query/trusty/server/released.current.txt"
         | 
| 199 | 
            +
                    type = @hvm ? 'hvm' : 'paravirtual'
         | 
| 200 | 
            +
                    data = open(url).read.split("\n").map{|l| l.split}.detect do |ary|
         | 
| 201 | 
            +
                      ary[4] == 'ebs' &&
         | 
| 202 | 
            +
                        ary[5] == 'amd64' &&
         | 
| 203 | 
            +
                        ary[6] == @region &&
         | 
| 204 | 
            +
                        ary.last == type
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
                    data[7]
         | 
| 207 | 
            +
                  end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                  def instance_ip
         | 
| 210 | 
            +
                    client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.public_ip_address
         | 
| 211 | 
            +
                  end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                  def security_group
         | 
| 214 | 
            +
                    group_name = 'linecook-global-ssh'
         | 
| 215 | 
            +
                    resp = client.describe_security_groups(filters: [{name: 'group-name', values: [group_name]}])
         | 
| 216 | 
            +
                    if resp.security_groups.length < 1
         | 
| 217 | 
            +
                      resp = client.create_security_group({
         | 
| 218 | 
            +
                        group_name: group_name,
         | 
| 219 | 
            +
                        description: "Allow global ssh for linecook temporary builder instances",
         | 
| 220 | 
            +
                      })
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                      resp = client.authorize_security_group_ingress({
         | 
| 223 | 
            +
                        group_name: group_name,
         | 
| 224 | 
            +
                        ip_protocol: "tcp",
         | 
| 225 | 
            +
                        from_port: 22,
         | 
| 226 | 
            +
                        to_port: 22,
         | 
| 227 | 
            +
                        cidr_ip: "0.0.0.0/0",
         | 
| 228 | 
            +
                      })
         | 
| 229 | 
            +
                    end
         | 
| 230 | 
            +
                    group_name
         | 
| 231 | 
            +
                  end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  def tag(id, **kwargs)
         | 
| 234 | 
            +
                    resp = client.create_tags(resources: [id], tags: kwargs.map{ |k,v| {key: k, value: v } })
         | 
| 235 | 
            +
                  end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                  def key_pair
         | 
| 238 | 
            +
                    pubkey = Linecook::SSH.public_key
         | 
| 239 | 
            +
                    resp = client.describe_key_pairs({
         | 
| 240 | 
            +
                      filters: [ { name: 'fingerprint', values: [Linecook::SSH.sshv2_fingerprint(pubkey)] } ]
         | 
| 241 | 
            +
                    })
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                    if resp.key_pairs.length >= 1
         | 
| 244 | 
            +
                      return resp.key_pairs.first.key_name
         | 
| 245 | 
            +
                    else
         | 
| 246 | 
            +
                      keyname = "linecook-#{SecureRandom.uuid}"
         | 
| 247 | 
            +
                      resp = client.import_key_pair({
         | 
| 248 | 
            +
                        key_name: keyname,
         | 
| 249 | 
            +
                        public_key_material: pubkey,
         | 
| 250 | 
            +
                      })
         | 
| 251 | 
            +
                      return keyname
         | 
| 252 | 
            +
                    end
         | 
| 253 | 
            +
                  end
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  def metadata(key)
         | 
| 256 | 
            +
                   (Timeout::timeout(1) { Net::HTTP.get(URI(File.join("http://169.254.169.254/latest/meta-data", key))) } rescue nil)
         | 
| 257 | 
            +
                  end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                  def wait_for_state(desired, timeout)
         | 
| 260 | 
            +
                    attempts = 0
         | 
| 261 | 
            +
                    state = nil
         | 
| 262 | 
            +
                    while attempts < timeout && state != desired
         | 
| 263 | 
            +
                      state = yield
         | 
| 264 | 
            +
                      attempts += 1
         | 
| 265 | 
            +
                      sleep(1)
         | 
| 266 | 
            +
                    end
         | 
| 267 | 
            +
                  end
         | 
| 268 | 
            +
                end
         | 
| 269 | 
            +
              end
         | 
| 270 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            require 'linecook/packager/ebs'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Linecook
         | 
| 4 | 
            +
              module Packager
         | 
| 5 | 
            +
                extend self
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def package(image)
         | 
| 8 | 
            +
                  provider.package(image)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              private
         | 
| 12 | 
            +
                def provider
         | 
| 13 | 
            +
                  name = Linecook::Config.load_config[:packager][:provider]
         | 
| 14 | 
            +
                  config = Linecook::Config.load_config[:packager][name]
         | 
| 15 | 
            +
                  case name
         | 
| 16 | 
            +
                  when :ebs
         | 
| 17 | 
            +
                    Linecook::Packager::EBS.new(**config)
         | 
| 18 | 
            +
                  else
         | 
| 19 | 
            +
                    fail "No packager implemented for for #{name}"
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -8,13 +8,14 @@ module Linecook | |
| 8 8 | 
             
              module Chef
         | 
| 9 9 | 
             
                extend self
         | 
| 10 10 |  | 
| 11 | 
            -
                def provision(build)
         | 
| 11 | 
            +
                def provision(build, role)
         | 
| 12 12 | 
             
                  chef_config = setup
         | 
| 13 | 
            +
                  role_config = Linecook::Config.load_config[:roles][role.to_sym]
         | 
| 13 14 | 
             
                  script = ChefProvisioner::Bootstrap.generate(
         | 
| 14 15 | 
             
                    node_name: chef_config[:node_name],
         | 
| 15 16 | 
             
                    chef_version: chef_config[:version] || nil,
         | 
| 16 17 | 
             
                    first_boot: {
         | 
| 17 | 
            -
                      run_list:  | 
| 18 | 
            +
                      run_list: role_config[:run_list]
         | 
| 18 19 | 
             
                    }
         | 
| 19 20 | 
             
                  )
         | 
| 20 21 |  | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            require 'securerandom'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'linecook/builder/build'
         | 
| 4 | 
            +
            require 'linecook/provisioner/chef-zero'
         | 
| 5 | 
            +
            require 'linecook/provisioner/packer'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Linecook
         | 
| 8 | 
            +
              module Baker
         | 
| 9 | 
            +
                extend self
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def bake(name: nil, image: nil, snapshot: nil, upload: nil, package: nil, build: nil)
         | 
| 12 | 
            +
                  build_agent = Linecook::Build.new(name, image: image)
         | 
| 13 | 
            +
                  provider(name).provision(build_agent, name) if build
         | 
| 14 | 
            +
                  snapshot = build_agent.snapshot(save: true) if snapshot ||  upload || package
         | 
| 15 | 
            +
                  Linecook::ImageManager.upload(snapshot) if upload || package
         | 
| 16 | 
            +
                  Linecook::Packager.package(snapshot) if package
         | 
| 17 | 
            +
                  build_agent.stop
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              private
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def provider(name)
         | 
| 23 | 
            +
                  provisioner = Linecook::Config.load_config[:roles][name.to_sym][:provisioner] || Linecook::Config.load_config[:provisioner][:default_provider]
         | 
| 24 | 
            +
                  case provisioner
         | 
| 25 | 
            +
                  when :chefzero
         | 
| 26 | 
            +
                    Linecook::Chef
         | 
| 27 | 
            +
                  when :packer
         | 
| 28 | 
            +
                    Linecook::Packer
         | 
| 29 | 
            +
                  else
         | 
| 30 | 
            +
                    fail "Unsupported provisioner #{provisioner}"
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            require 'json'
         | 
| 2 | 
            +
            require 'tempfile'
         | 
| 3 | 
            +
            require 'mkmf'
         | 
| 4 | 
            +
            require 'fileutils'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'linecook/util/executor'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Linecook
         | 
| 10 | 
            +
              module Packer
         | 
| 11 | 
            +
                def self.provision(build, role)
         | 
| 12 | 
            +
                  Runner.new(build, role).run
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                class Runner
         | 
| 16 | 
            +
                  SOURCE_URL = 'https://releases.hashicorp.com/packer/'
         | 
| 17 | 
            +
                  PACKER_VERSION = '0.8.6'
         | 
| 18 | 
            +
                  PACKER_PATH = File.join(Linecook::Config::LINECOOK_HOME, 'bin', 'packer')
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  include Executor
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def initialize(build, role)
         | 
| 23 | 
            +
                    role_config = Linecook::Config.load_config[:roles][role.to_sym]
         | 
| 24 | 
            +
                    @packer = packer_path
         | 
| 25 | 
            +
                    @build = build
         | 
| 26 | 
            +
                    @template = role_config[:template_path]
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def run
         | 
| 30 | 
            +
                    @build.start
         | 
| 31 | 
            +
                    packer_template = inject_builder
         | 
| 32 | 
            +
                    Tempfile.open('packer-linecook') do |template|
         | 
| 33 | 
            +
                      template.write(JSON.dump(packer_template))
         | 
| 34 | 
            +
                      template.flush
         | 
| 35 | 
            +
                      execute("#{@packer} build #{template.path}", sudo: false)
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def packer_path
         | 
| 42 | 
            +
                    found = File.exists?(PACKER_PATH) ? PACKER_PATH : find_executable('packer')
         | 
| 43 | 
            +
                    path = if found
         | 
| 44 | 
            +
                      version = execute("#{found} --version", sudo: false, capture: true)
         | 
| 45 | 
            +
                      Gem::Version.new(version) >= Gem::Version.new(PACKER_VERSION) ? found : nil
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    path ||= get_packer
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def get_packer
         | 
| 52 | 
            +
                    puts "packer too old (<#{PACKER_VERSION}) or not present, getting latest packer"
         | 
| 53 | 
            +
                    arch = 1.size == 8 ? 'amd64' : '386'
         | 
| 54 | 
            +
                    path = File.join(File.dirname(PACKER_PATH), 'packer.zip')
         | 
| 55 | 
            +
                    url = File.join(SOURCE_URL, PACKER_VERSION, "packer_#{PACKER_VERSION}_#{Linecook::Config.platform}_#{arch}.zip")
         | 
| 56 | 
            +
                    Linecook::Downloader.download(url, path)
         | 
| 57 | 
            +
                    Linecook::Downloader.unzip(path)
         | 
| 58 | 
            +
                    PACKER_PATH
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def inject_builder
         | 
| 62 | 
            +
                    packer_template = JSON.load(File.read(@template)).symbolize_keys
         | 
| 63 | 
            +
                    packer_template.merge(builders: null_builder)
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def null_builder
         | 
| 67 | 
            +
                    [ communicator.merge(type: 'null') ]
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def communicator
         | 
| 71 | 
            +
                    {
         | 
| 72 | 
            +
                      ssh_host: @build.ssh.hostname,
         | 
| 73 | 
            +
                      ssh_username: @build.ssh.username,
         | 
| 74 | 
            +
                      ssh_private_key_file: @build.ssh.keyfile,
         | 
| 75 | 
            +
                      ssh_bastion_host: Linecook::Builder.ssh.hostname,
         | 
| 76 | 
            +
                      ssh_bastion_username: Linecook::Builder.ssh.username,
         | 
| 77 | 
            +
                      ssh_bastion_private_key_file: Linecook::Builder.ssh.keyfile,
         | 
| 78 | 
            +
                    }
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            require 'yaml'
         | 
| 2 | 
            +
            require 'json'
         | 
| 2 3 | 
             
            require 'fileutils'
         | 
| 3 4 |  | 
| 4 5 | 
             
            require 'xhyve'
         | 
| @@ -19,13 +20,52 @@ module Linecook | |
| 19 20 | 
             
                    username: 'ubuntu',
         | 
| 20 21 | 
             
                    password: 'ubuntu'
         | 
| 21 22 | 
             
                  },
         | 
| 22 | 
            -
                   | 
| 23 | 
            -
                     | 
| 24 | 
            -
                     | 
| 25 | 
            -
             | 
| 23 | 
            +
                  provisioner: {
         | 
| 24 | 
            +
                    default_provider: :chefzero,
         | 
| 25 | 
            +
                    default_image: :base_image,
         | 
| 26 | 
            +
                  },
         | 
| 27 | 
            +
                  image: {
         | 
| 28 | 
            +
                    provider: {
         | 
| 29 | 
            +
                      public: :github,
         | 
| 30 | 
            +
                      private: :s3,
         | 
| 31 | 
            +
                    },
         | 
| 32 | 
            +
                    images: {
         | 
| 33 | 
            +
                      live_iso: {
         | 
| 34 | 
            +
                        name: 'livesys.iso',
         | 
| 35 | 
            +
                        profile: :public,
         | 
| 36 | 
            +
                      },
         | 
| 37 | 
            +
                      live_image: {
         | 
| 38 | 
            +
                        name: 'livesys.squashfs',
         | 
| 39 | 
            +
                        profile: :public,
         | 
| 40 | 
            +
                      },
         | 
| 41 | 
            +
                      base_image: {
         | 
| 42 | 
            +
                        name: 'ubuntu-base.squashfs',
         | 
| 43 | 
            +
                        profile: :public,
         | 
| 44 | 
            +
                      }
         | 
| 45 | 
            +
                    }
         | 
| 46 | 
            +
                  },
         | 
| 47 | 
            +
                  packager: {
         | 
| 48 | 
            +
                    provider: :ebs,
         | 
| 49 | 
            +
                    ebs: {
         | 
| 50 | 
            +
                      hvm: true,
         | 
| 51 | 
            +
                      size: 10,
         | 
| 52 | 
            +
                      region: 'us-east-1'
         | 
| 53 | 
            +
                    }
         | 
| 54 | 
            +
                  },
         | 
| 55 | 
            +
                  roles: {
         | 
| 26 56 | 
             
                  }
         | 
| 27 57 | 
             
                }
         | 
| 28 58 |  | 
| 59 | 
            +
                def secrets
         | 
| 60 | 
            +
                  @secrets ||= begin
         | 
| 61 | 
            +
                    if File.exists?('secrets.ejson')
         | 
| 62 | 
            +
                      JSON.load(`ejson decrypt secrets.ejson`)
         | 
| 63 | 
            +
                    else
         | 
| 64 | 
            +
                      {}
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 29 69 | 
             
                def setup
         | 
| 30 70 | 
             
                  FileUtils.mkdir_p(LINECOOK_HOME)
         | 
| 31 71 | 
             
                  File.write(DEFAULT_CONFIG_PATH, YAML.dump(DEFAULT_CONFIG)) unless File.exist?(DEFAULT_CONFIG_PATH)
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require 'open-uri'
         | 
| 2 | 
            +
            require 'fileutils'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'zip'
         | 
| 5 | 
            +
            require 'ruby-progressbar'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Linecook
         | 
| 8 | 
            +
              module Downloader
         | 
| 9 | 
            +
                def self.download(url, path)
         | 
| 10 | 
            +
                  FileUtils.mkdir_p(File.dirname(path))
         | 
| 11 | 
            +
                  File.open(path, 'w') do |f|
         | 
| 12 | 
            +
                    pbar = ProgressBar.create(title: File.basename(path), total: nil)
         | 
| 13 | 
            +
                    IO.copy_stream(open(url,
         | 
| 14 | 
            +
                                        content_length_proc: lambda do|t|
         | 
| 15 | 
            +
                                          pbar.total = t if t && 0 < t
         | 
| 16 | 
            +
                                        end,
         | 
| 17 | 
            +
                                        progress_proc: lambda do|s|
         | 
| 18 | 
            +
                                          pbar.progress = s
         | 
| 19 | 
            +
                                        end), f)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def self.unzip(source, dest: nil)
         | 
| 24 | 
            +
                  puts "Extracting #{source}..."
         | 
| 25 | 
            +
                  dest ||= File.dirname(source)
         | 
| 26 | 
            +
                  Zip::File.open(source) do |zip_file|
         | 
| 27 | 
            +
                    zip_file.each do |f|
         | 
| 28 | 
            +
                      file_path = File.join(dest, f.name)
         | 
| 29 | 
            +
                      FileUtils.mkdir_p(File.dirname(file_path))
         | 
| 30 | 
            +
                      zip_file.extract(f, file_path)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            module Linecook
         | 
| 2 | 
            +
              module Executor
         | 
| 3 | 
            +
                def capture(command, sudo: true)
         | 
| 4 | 
            +
                  execute(command, sudo: sudo, capture: true)
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def test(check)
         | 
| 8 | 
            +
                  if @remote
         | 
| 9 | 
            +
                    return @remote.test(check)
         | 
| 10 | 
            +
                  else
         | 
| 11 | 
            +
                    `#{check}`
         | 
| 12 | 
            +
                    return $?.exitstatus == 0
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def execute(command, sudo: true, capture: false)
         | 
| 17 | 
            +
                  command = "sudo #{command}" if sudo
         | 
| 18 | 
            +
                  if @remote
         | 
| 19 | 
            +
                    if capture
         | 
| 20 | 
            +
                      return @remote.capture(command)
         | 
| 21 | 
            +
                    else
         | 
| 22 | 
            +
                      @remote.run(command)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  else
         | 
| 25 | 
            +
                    if capture
         | 
| 26 | 
            +
                      return `#{command}`
         | 
| 27 | 
            +
                    else
         | 
| 28 | 
            +
                      system(command)
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -1,9 +1,11 @@ | |
| 1 | 
            +
            require 'openssl'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'sshkit'
         | 
| 2 4 | 
             
            require 'sshkit/dsl'
         | 
| 3 5 | 
             
            require 'net/ssh'
         | 
| 4 6 | 
             
            require 'net/ssh/proxy/command'
         | 
| 5 7 |  | 
| 6 | 
            -
            require 'linecook/config'
         | 
| 8 | 
            +
            require 'linecook/util/config'
         | 
| 7 9 |  | 
| 8 10 | 
             
            module Linecook
         | 
| 9 11 | 
             
              class SSHKit::Formatter::Linecook < SSHKit::Formatter::Pretty
         | 
| @@ -48,14 +50,40 @@ module Linecook | |
| 48 50 | 
             
              end
         | 
| 49 51 |  | 
| 50 52 | 
             
              class SSH
         | 
| 53 | 
            +
                MAX_RETRIES = 5
         | 
| 54 | 
            +
                attr_reader :username, :password, :hostname, :keyfile
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def self.private_key
         | 
| 57 | 
            +
                  userkey = File.expand_path("~/.ssh/id_rsa")
         | 
| 58 | 
            +
                  dedicated_key = File.join(Linecook::Config::LINECOOK_HOME, 'linecook_ssh.pem')
         | 
| 59 | 
            +
                  unless File.exists?(dedicated_key)
         | 
| 60 | 
            +
                    File.write(dedicated_key, SSHKey.generate.private_key)
         | 
| 61 | 
            +
                    FileUtils.chmod(0600, dedicated_key)
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                  File.exists?(userkey) ? userkey : dedicated_key
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def self.public_key(keyfile: nil)
         | 
| 67 | 
            +
                  SSHKey.new(File.read(keyfile || private_key)).ssh_public_key
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                # Generate a fingerprint for an SSHv2 key used by amazon, yes, this is ugly
         | 
| 71 | 
            +
                def self.sshv2_fingerprint(key)
         | 
| 72 | 
            +
                  _, blob = key.split(/ /)
         | 
| 73 | 
            +
                  blob = blob.unpack("m*").first
         | 
| 74 | 
            +
                  reader = Net::SSH::Buffer.new(blob)
         | 
| 75 | 
            +
                  k=reader.read_key
         | 
| 76 | 
            +
                  OpenSSL::Digest.new('md5',k.to_der).hexdigest.scan(/../).join(":")
         | 
| 77 | 
            +
                end
         | 
| 51 78 |  | 
| 52 | 
            -
                attr_reader :username, :hostname
         | 
| 53 79 | 
             
                def initialize(hostname, username: 'ubuntu', password: nil, keyfile: nil, proxy: nil)
         | 
| 54 80 | 
             
                  @username = username
         | 
| 55 81 | 
             
                  @password = password
         | 
| 56 82 | 
             
                  @hostname = hostname
         | 
| 57 83 | 
             
                  @keyfile = keyfile
         | 
| 58 84 | 
             
                  @proxy = proxy_command(proxy) if proxy
         | 
| 85 | 
            +
                  wait_for_connection
         | 
| 86 | 
            +
                  setup_ssh_key if @keyfile
         | 
| 59 87 | 
             
                end
         | 
| 60 88 |  | 
| 61 89 | 
             
                def forward(local, remote:nil)
         | 
| @@ -107,8 +135,37 @@ module Linecook | |
| 107 135 | 
             
                  end
         | 
| 108 136 | 
             
                end
         | 
| 109 137 |  | 
| 138 | 
            +
                def download(path, local: nil)
         | 
| 139 | 
            +
                  on linecook_host do |_host|
         | 
| 140 | 
            +
                    download! path, local || File.basename(path)
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
             | 
| 110 144 | 
             
                private
         | 
| 111 145 |  | 
| 146 | 
            +
                def wait_for_connection
         | 
| 147 | 
            +
                  puts "Waiting for SSH connection"
         | 
| 148 | 
            +
                  attempts = 0
         | 
| 149 | 
            +
                  while attempts < MAX_RETRIES
         | 
| 150 | 
            +
                    begin
         | 
| 151 | 
            +
                      run("echo connected")
         | 
| 152 | 
            +
                      return
         | 
| 153 | 
            +
                    rescue SSHKit::Runner::ExecuteError
         | 
| 154 | 
            +
                      puts "Retrying SSH connection"
         | 
| 155 | 
            +
                      sleep(5)
         | 
| 156 | 
            +
                      attempts += 1
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def setup_ssh_key
         | 
| 162 | 
            +
                  pubkey = Linecook::SSH.public_key(keyfile: @keyfile)
         | 
| 163 | 
            +
                  config = Linecook::Config.load_config[:builder]
         | 
| 164 | 
            +
                  run("mkdir -p /home/#{config[:username]}/.ssh")
         | 
| 165 | 
            +
                  upload(pubkey, "/home/#{config[:username]}/.ssh/authorized_keys")
         | 
| 166 | 
            +
                end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
             | 
| 112 169 | 
             
                def linecook_host
         | 
| 113 170 | 
             
                  @host ||= begin
         | 
| 114 171 | 
             
                    host = SSHKit::Host.new(user: @username, hostname: @hostname)
         | 
    
        data/lib/linecook/version.rb
    CHANGED
    
    
    
        data/lib/linecook.rb
    CHANGED
    
    | @@ -2,7 +2,8 @@ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__)) | |
| 2 2 |  | 
| 3 3 | 
             
            require 'active_support/all'
         | 
| 4 4 | 
             
            require 'linecook/version'
         | 
| 5 | 
            -
            require 'linecook/config'
         | 
| 6 | 
            -
            require 'linecook/ | 
| 7 | 
            -
            require 'linecook/ | 
| 8 | 
            -
            require 'linecook/ | 
| 5 | 
            +
            require 'linecook/util/config'
         | 
| 6 | 
            +
            require 'linecook/image/manager'
         | 
| 7 | 
            +
            require 'linecook/builder/manager'
         | 
| 8 | 
            +
            require 'linecook/provisioner/manager'
         | 
| 9 | 
            +
            require 'linecook/packager/manager'
         |