magellan-cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +60 -0
- data/LICENSE.txt +22 -0
- data/README.md +109 -0
- data/Rakefile +7 -0
- data/bin/magellan-cli +13 -0
- data/launch_options.json +7 -0
- data/lib/magellan/cli/base.rb +50 -0
- data/lib/magellan/cli/command.rb +29 -0
- data/lib/magellan/cli/direct.rb +32 -0
- data/lib/magellan/cli/errors.rb +14 -0
- data/lib/magellan/cli/http.rb +103 -0
- data/lib/magellan/cli/login.rb +134 -0
- data/lib/magellan/cli/resources/base.rb +136 -0
- data/lib/magellan/cli/resources/client_version.rb +15 -0
- data/lib/magellan/cli/resources/container_image.rb +13 -0
- data/lib/magellan/cli/resources/container_instance.rb +25 -0
- data/lib/magellan/cli/resources/function_unit.rb +27 -0
- data/lib/magellan/cli/resources/host_instance.rb +56 -0
- data/lib/magellan/cli/resources/project.rb +22 -0
- data/lib/magellan/cli/resources/stage.rb +69 -0
- data/lib/magellan/cli/resources/worker_version.rb +41 -0
- data/lib/magellan/cli/resources.rb +27 -0
- data/lib/magellan/cli/sample_launch_options.json +7 -0
- data/lib/magellan/cli/ssl.rb +35 -0
- data/lib/magellan/cli/version.rb +5 -0
- data/lib/magellan/cli.rb +23 -0
- data/magellan-cli.gemspec +29 -0
- data/spec/magellan/cli/Magellan.yml +29 -0
- data/spec/magellan/cli/login_page.html +42 -0
- data/spec/magellan/cli/login_spec.rb +46 -0
- data/spec/magellan/cli/resources/project_spec.rb +39 -0
- data/spec/magellan/cli_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- metadata +187 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli"
|
3
|
+
|
4
|
+
require 'httpclient'
|
5
|
+
require 'json'
|
6
|
+
require 'uri'
|
7
|
+
require 'nokogiri'
|
8
|
+
|
9
|
+
require 'active_support/core_ext/hash/keys'
|
10
|
+
|
11
|
+
module Magellan
|
12
|
+
module Cli
|
13
|
+
class Login
|
14
|
+
|
15
|
+
PRODUCTION_HTTP_PORT = 80
|
16
|
+
PRODUCTION_HTTPS_PORT = 443
|
17
|
+
|
18
|
+
DEFAULT_HTTP_PORT = (ENV['DEFAULT_HTTP_PORT' ] || 80).to_i
|
19
|
+
DEFAULT_HTTPS_PORT = (ENV['DEFAULT_HTTPS_PORT'] || 443).to_i
|
20
|
+
|
21
|
+
attr_reader :httpclient
|
22
|
+
attr_reader :base_http_url
|
23
|
+
attr_reader :base_https_url
|
24
|
+
attr_reader :ssl_disabled
|
25
|
+
|
26
|
+
attr_reader :auth_token
|
27
|
+
|
28
|
+
# Magellan::Cli::Loginのコンストラクタです。
|
29
|
+
#
|
30
|
+
# @param [String] base_url_or_host 接続先の基準となるURLあるいはホスト名
|
31
|
+
# @param [Hash] options オプション
|
32
|
+
# @option options [String] :api_version APIのバージョン。デフォルトは "1.0.0"
|
33
|
+
# @option options [Boolean] :ssl_disabled SSLを無効にするかどうか。
|
34
|
+
# @option options [Integer] :https_port HTTPSで接続する際の接続先のポート番号
|
35
|
+
def initialize(base_url_or_host = nil, options = {})
|
36
|
+
base_url_or_host ||= (ENV["MAGELLAN_SITE"] || "http://localhost:3000/")
|
37
|
+
if base_url_or_host =~ URI.regexp
|
38
|
+
@base_http_url = base_url_or_host.sub(/\/\Z/, '')
|
39
|
+
uri = URI.parse(@base_http_url)
|
40
|
+
else
|
41
|
+
if config_path = search_file(".magellan-cli.yml")
|
42
|
+
config = YAML.load_file_with_erb(config_path)
|
43
|
+
options = config[base_url_or_host.to_s].deep_symbolize_keys.update(options)
|
44
|
+
end
|
45
|
+
uri = URI::Generic.build({scheme: "http", host: base_url_or_host, port: DEFAULT_HTTP_PORT}.update(options))
|
46
|
+
@base_http_url = uri.to_s
|
47
|
+
end
|
48
|
+
@ssl_disabled = options.delete(:ssl_disabled)
|
49
|
+
@ssl_disabled ||= (uri.port == 3000)
|
50
|
+
@base_https_url = @ssl_disabled ? @base_http_url : build_https_url(uri, options[:https_port])
|
51
|
+
# @api_version = options[:api_version] || "1.0.0"
|
52
|
+
|
53
|
+
@httpclient = HTTPClient.new
|
54
|
+
@httpclient.ssl_config.verify_mode = nil # 自己署名の証明書をOKにする
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_https_url(uri, port = nil)
|
58
|
+
uri.scheme = "https"
|
59
|
+
uri.port = port || (uri.port == PRODUCTION_HTTP_PORT ? PRODUCTION_HTTPS_PORT : uri.port + 1)
|
60
|
+
uri.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def login_form_url
|
64
|
+
@login_form_url ||= base_https_url + "/users/sign_in.html"
|
65
|
+
end
|
66
|
+
def login_url
|
67
|
+
@login_url ||= base_https_url + "/api/sign_in.json"
|
68
|
+
end
|
69
|
+
|
70
|
+
# magellan-apiサーバに接続してログインの検証と処理を行います。
|
71
|
+
#
|
72
|
+
# @param [Hash] extra オプション
|
73
|
+
# @return [Integer] サーバが返したレスポンスのステータスを返します
|
74
|
+
# @return [String] サーバが返したレスポンスのbodyを返します
|
75
|
+
def login_and_status
|
76
|
+
@auth_token ||= get_auth_token
|
77
|
+
params = {
|
78
|
+
"user" => {"email" => "magellan@groovenauts.jp", "password" => "password"},
|
79
|
+
"authenticity_token" => @auth_token
|
80
|
+
}.to_json
|
81
|
+
res2 = Ssl.retry_on_ssl_error("login"){ @httpclient.post(login_url, params, JSON_HEADER) }
|
82
|
+
return res2.status, res2.body
|
83
|
+
end
|
84
|
+
|
85
|
+
# GSSサーバに接続してログインの検証と処理を行います。
|
86
|
+
#
|
87
|
+
# @param [Hash] extra オプション
|
88
|
+
# @option extra [Integer] :device_type デバイス種別
|
89
|
+
# @option extra [Integer] :device_id デバイス識別子
|
90
|
+
# @return [Boolean] ログインに成功した場合はtrue、失敗した場合はfalse
|
91
|
+
def login
|
92
|
+
status, _ = login_and_status
|
93
|
+
case status
|
94
|
+
when 200...300 then true
|
95
|
+
else false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# GSSサーバに接続してログインの検証と処理を行います。
|
100
|
+
#
|
101
|
+
# @param [Hash] extra オプション
|
102
|
+
# @see #login
|
103
|
+
# @return ログインに成功した場合は自身のオブジェクト返します。失敗した場合はLibgss::Network::Errorがraiseされます。
|
104
|
+
def login!
|
105
|
+
status, body = login_and_status
|
106
|
+
case status
|
107
|
+
when 200...300 then return self
|
108
|
+
else raise LoginError, "status: #{status} #{body}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_auth_token
|
113
|
+
res = Ssl.retry_on_ssl_error("login_form"){ @httpclient.get(login_form_url) }
|
114
|
+
doc = Nokogiri::HTML.parse(res.body, login_form_url, res.body_encoding.to_s)
|
115
|
+
node = doc.xpath('//input[@name="authenticity_token"]').first
|
116
|
+
node.attribute('value').value
|
117
|
+
end
|
118
|
+
|
119
|
+
# @httpclient.inspectの戻り値の文字列が巨大なので、inspectで出力しないようにします。
|
120
|
+
def inspect
|
121
|
+
r = "#<#{self.class.name}:#{self.object_id} "
|
122
|
+
fields = (instance_variables - [:@httpclient]).map{|f| "#{f}=#{instance_variable_get(f).inspect}"}
|
123
|
+
r << fields.join(", ") << ">"
|
124
|
+
end
|
125
|
+
|
126
|
+
def search_file(basename)
|
127
|
+
dirs = [".", "./config", ENV['HOME']].join(",")
|
128
|
+
Dir["{#{dirs}}/#{basename}"].select{|path| File.readable?(path)}.first
|
129
|
+
end
|
130
|
+
private :search_file
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
9
|
+
|
10
|
+
module Magellan
|
11
|
+
module Cli
|
12
|
+
module Resources
|
13
|
+
|
14
|
+
class NotSelected < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class NotFound < StandardError
|
18
|
+
end
|
19
|
+
|
20
|
+
class Base < ::Magellan::Cli::Http
|
21
|
+
|
22
|
+
no_commands do
|
23
|
+
|
24
|
+
def load_selections
|
25
|
+
File.readable?(".magellan-cli") ? YAML.load_file(".magellan-cli") : {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_selection(name)
|
29
|
+
sel = load_selections
|
30
|
+
s = sel[name]
|
31
|
+
raise NotSelected, "No #{name} selected" unless s
|
32
|
+
return s
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_selections(hash = nil)
|
36
|
+
sel = load_selections
|
37
|
+
sel.update(hash) if hash
|
38
|
+
yield(sel) if block_given?
|
39
|
+
open(".magellan-cli", "w") do |f|
|
40
|
+
YAML.dump(sel, f)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_query(hash)
|
45
|
+
@@no ||= 0
|
46
|
+
r = {}
|
47
|
+
hash.each do |key, value|
|
48
|
+
@@no += 1
|
49
|
+
r["f[#{key}][#{@@no}][o]"] = "is"
|
50
|
+
r["f[#{key}][#{@@no}][v]"] = value
|
51
|
+
end
|
52
|
+
return r
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_query
|
56
|
+
sel = load_selections
|
57
|
+
q = {}
|
58
|
+
(self.class.resource_dependency || {}).each do |f, res|
|
59
|
+
r = sel[res]
|
60
|
+
raise NotSelected, "No #{f} selected" unless r
|
61
|
+
q[f] = r["id"]
|
62
|
+
end
|
63
|
+
return build_query(q)
|
64
|
+
end
|
65
|
+
|
66
|
+
DEFAULT_SELECTION_FIELDS = %w[id name].freeze
|
67
|
+
|
68
|
+
def update_first_result(name, path, query, fields = DEFAULT_SELECTION_FIELDS)
|
69
|
+
results = get_json(path, query)
|
70
|
+
raise NotFound, "#{name} not found for #{query.inspect}" if results.blank? || results.first.blank?
|
71
|
+
r = results.first
|
72
|
+
obj = fields.each_with_object({}) do |f, d|
|
73
|
+
d[f] = r[f]
|
74
|
+
end
|
75
|
+
update_selections(name => obj)
|
76
|
+
return r
|
77
|
+
end
|
78
|
+
|
79
|
+
def list
|
80
|
+
r = get_json("/admin/#{self.class.resource_name}.json", default_query)
|
81
|
+
$stdout.puts(JSON.pretty_generate(r) << "\nTotal: #{r.length}")
|
82
|
+
end
|
83
|
+
|
84
|
+
def show(id)
|
85
|
+
r = get_json("/admin/#{self.class.resource_name}/#{id}.json")
|
86
|
+
$stdout.puts(JSON.pretty_generate(r))
|
87
|
+
end
|
88
|
+
|
89
|
+
def select(name)
|
90
|
+
q = build_query("name" => name).update(default_query)
|
91
|
+
update_first_result(self.class.resource_name, "/admin/#{self.class.resource_name}.json", q)
|
92
|
+
end
|
93
|
+
|
94
|
+
def deselect
|
95
|
+
update_selections do |s|
|
96
|
+
s.delete(self.class.resource_name)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.inherited(klass)
|
101
|
+
base_name = klass.name.split(/::/).last
|
102
|
+
res_name = base_name.underscore
|
103
|
+
klass.module_eval <<-EOM
|
104
|
+
no_commands do
|
105
|
+
cattr_accessor :resource_name
|
106
|
+
cattr_accessor :resource_dependency
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "list", "list #{base_name}"
|
110
|
+
def list
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
desc "show ID", "show #{base_name} specified by ID"
|
115
|
+
def show(id)
|
116
|
+
super(id)
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "select NAME", "select #{base_name} by NAME"
|
120
|
+
def select(name)
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
desc "deselect", "deselect #{base_name}"
|
125
|
+
def deselect
|
126
|
+
super
|
127
|
+
end
|
128
|
+
EOM
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class ClientVersion < Base
|
9
|
+
self.resource_name = "client_version"
|
10
|
+
self.resource_dependency = {"stage" => "stage"}
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class ContainerInstance < Base
|
9
|
+
self.resource_name = "container_instance"
|
10
|
+
self.resource_dependency = {"host" => "host_instance"}
|
11
|
+
|
12
|
+
desc "start ID", "start container instance specified by ID"
|
13
|
+
def start(id)
|
14
|
+
post_json("/admin/container_instance/#{id}/status_control.json", {})
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "stop ID", "stop container instance specified by ID"
|
18
|
+
def stop(id)
|
19
|
+
delete("/admin/container_instance/#{id}/status_control.json")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class FunctionUnit < Base
|
9
|
+
self.resource_name = "function_unit"
|
10
|
+
self.resource_dependency = {"stage" => "stage-version"}
|
11
|
+
|
12
|
+
desc "create [ATTRIBUTES]", "create FunctionUnit with ATTRIBUTES (filepath or JSON text)"
|
13
|
+
def create(attrs = nil)
|
14
|
+
stage_id = load_selection("stage-version")
|
15
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
16
|
+
params = {
|
17
|
+
"function_unit" => {
|
18
|
+
"stage_version_id" => stage_id,
|
19
|
+
}.update(attrs)
|
20
|
+
}
|
21
|
+
post_json("/admin/function_unit/new.js", params)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class HostInstance < Base
|
9
|
+
self.resource_name = "host_instance"
|
10
|
+
self.resource_dependency = {"stage" => "stage-version"}
|
11
|
+
|
12
|
+
desc "sample_launch_options", "dump sample launch_options for create"
|
13
|
+
def sample_launch_options
|
14
|
+
$stdout.puts File.read(File.expand_path("../sample_launch_options.json", __FILE__))
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "create NAME [LAUNCH_OPTIONS]", "create Host Instance"
|
18
|
+
def create(name, launch_options = nil)
|
19
|
+
stage_id = load_selection("stage-version")
|
20
|
+
launch_options = JSON.parse(File.readable?(launch_options) ? File.read(launch_options) : launch_options)
|
21
|
+
|
22
|
+
params = {
|
23
|
+
"host_instance" => {
|
24
|
+
"stage_id" => stage_id,
|
25
|
+
"name" => name,
|
26
|
+
"launch_options_yaml" => YAML.dump(launch_options),
|
27
|
+
}
|
28
|
+
}
|
29
|
+
post_json("/admin/host_instance/new.js", params)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "launch", "launch host instance"
|
33
|
+
def launch
|
34
|
+
id = load_selection(self.class.resource_name)["id"]
|
35
|
+
post_json("/admin/host_instance/#{id}/status_control.json", {})
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "prepare", "prepare host instance"
|
39
|
+
def prepare
|
40
|
+
id = load_selection(self.class.resource_name)["id"]
|
41
|
+
put_json("/admin/host_instance/#{id}/status_control.json", {})
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "stop", "stop host instance"
|
45
|
+
def stop
|
46
|
+
id = load_selection(self.class.resource_name)["id"]
|
47
|
+
delete("/admin/host_instance/#{id}/status_control.json")
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class Project < Base
|
9
|
+
self.resource_name = "project"
|
10
|
+
self.resource_dependency = nil
|
11
|
+
|
12
|
+
desc "update ATTRIBUTES", "update project with ATTRIBUTES"
|
13
|
+
def update(attrs)
|
14
|
+
s = load_selection("project")
|
15
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
16
|
+
put_json("/admin/magellan~auth~project/#{s['id']}/edit", {"magellan_auth_project" => attrs})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Magellan
|
6
|
+
module Cli
|
7
|
+
module Resources
|
8
|
+
|
9
|
+
class Stage < Base
|
10
|
+
self.resource_name = "stage~title"
|
11
|
+
self.resource_dependency = {"project" => "project"}
|
12
|
+
|
13
|
+
desc "select NAME", "select Stage named by NAME"
|
14
|
+
def select(name)
|
15
|
+
q = build_query("name" => name).update(default_query)
|
16
|
+
r = update_first_result("stage", "/admin/stage~title.json", q)
|
17
|
+
|
18
|
+
# # current
|
19
|
+
# q = build_query("title" => r["id"], "phase" => 2) # 2: current
|
20
|
+
# update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
21
|
+
|
22
|
+
# # workspace
|
23
|
+
q = build_query("title" => r["id"], "phase" => 1) # 1: workspace
|
24
|
+
update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "planning", "switch to planning to build next released version"
|
28
|
+
def planning
|
29
|
+
switch_version(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "current", "switch to current released version"
|
33
|
+
def current
|
34
|
+
switch_version(2)
|
35
|
+
end
|
36
|
+
|
37
|
+
no_commands do
|
38
|
+
def switch_version(phase)
|
39
|
+
s = load_selection("stage")
|
40
|
+
q = build_query("title" => s["id"], "phase" => phase) # 1: workspace, 2: current, 3: used
|
41
|
+
update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "prepare", "prepare containers"
|
46
|
+
def prepare
|
47
|
+
s = load_selection("stage")
|
48
|
+
id = s["id"]
|
49
|
+
post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "prepare_containers"})
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "update ATTRIBUTES", "update stage with ATTRIBUTES"
|
53
|
+
def update(attrs)
|
54
|
+
s = load_selection("stage")
|
55
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
56
|
+
put_json("/admin/stage~title/#{s['id']}/edit", {"stage_title" => attrs})
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "release_now", "release changes now"
|
60
|
+
def release_now
|
61
|
+
s = load_selection("stage")
|
62
|
+
id = s["id"]
|
63
|
+
post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "release_now"})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|