workspaces.go 24 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  1. // Copyright Joyent, Inc.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "errors"
  10. "fmt"
  11. "net/url"
  12. "sort"
  13. "strconv"
  14. "strings"
  15. "time"
  16. "github.com/gofrs/uuid"
  17. "github.com/jawher/mow.cli"
  18. "github.com/olekukonko/tablewriter"
  19. )
  20. type Workspaces struct {
  21. *Conch
  22. }
  23. func (c *Conch) Workspaces() *Workspaces {
  24. return &Workspaces{c}
  25. }
  26. /****/
  27. var WorkspaceRoleList = []string{"admin", "rw", "ro"}
  28. func prettyWorkspaceRoleList() string {
  29. return strings.Join(WorkspaceRoleList, ", ")
  30. }
  31. func okWorkspaceRole(role string) bool {
  32. for _, b := range WorkspaceRoleList {
  33. if role == b {
  34. return true
  35. }
  36. }
  37. return false
  38. }
  39. /****/
  40. type WorkspaceAndRole struct {
  41. ID uuid.UUID `json:"id"`
  42. Name string `json:"name"`
  43. Description string `json:"description,omitempty"`
  44. ParentID uuid.UUID `json:"parent_id,omitempty"`
  45. Role string `json:"role"`
  46. RoleVia uuid.UUID `json:"role_via"`
  47. // These are for user friendly variants of those UUIDs
  48. Parent string `json:"parent"`
  49. Via string `json:"via"`
  50. }
  51. func (w WorkspaceAndRole) String() string {
  52. if API.JsonOnly {
  53. return API.AsJSON(w)
  54. }
  55. t, err := NewTemplate().Parse(workspaceTemplate)
  56. if err != nil {
  57. panic(err)
  58. }
  59. buf := new(bytes.Buffer)
  60. if err := t.Execute(buf, w); err != nil {
  61. panic(err)
  62. }
  63. return buf.String()
  64. }
  65. type WorkspaceAndRoles []WorkspaceAndRole
  66. func (w WorkspaceAndRoles) Len() int {
  67. return len(w)
  68. }
  69. func (w WorkspaceAndRoles) Swap(i, j int) {
  70. w[i], w[j] = w[j], w[i]
  71. }
  72. func (w WorkspaceAndRoles) Less(i, j int) bool {
  73. return w[i].Name < w[j].Name
  74. }
  75. func (w WorkspaceAndRoles) String() string {
  76. sort.Sort(w)
  77. if API.JsonOnly {
  78. return API.AsJSON(w)
  79. }
  80. tableString := &strings.Builder{}
  81. table := tablewriter.NewWriter(tableString)
  82. TableToMarkdown(table)
  83. table.SetHeader([]string{
  84. "Name",
  85. "Role",
  86. "Description",
  87. "Role Via",
  88. "Parent",
  89. })
  90. for _, ws := range w {
  91. table.Append([]string{
  92. ws.Name,
  93. ws.Role,
  94. ws.Description,
  95. ws.Via,
  96. ws.Parent,
  97. })
  98. }
  99. table.Render()
  100. return tableString.String()
  101. }
  102. /****/
  103. func (w *Workspaces) GetAll() WorkspaceAndRoles {
  104. list := make(WorkspaceAndRoles, 0)
  105. res := w.Do(w.Sling().Get("/workspace"))
  106. if ok := res.Parse(&list); !ok {
  107. panic(res)
  108. }
  109. ret := make(WorkspaceAndRoles, 0)
  110. cache := make(map[uuid.UUID]string)
  111. for _, ws := range list {
  112. if (ws.ParentID != uuid.UUID{}) {
  113. if _, ok := cache[ws.ParentID]; !ok {
  114. cache[ws.ParentID] = w.Get(ws.ParentID).Name
  115. }
  116. ws.Parent = cache[ws.ParentID]
  117. }
  118. if (ws.RoleVia != uuid.UUID{}) {
  119. if _, ok := cache[ws.RoleVia]; !ok {
  120. cache[ws.RoleVia] = w.Get(ws.RoleVia).Name
  121. }
  122. ws.Via = cache[ws.RoleVia]
  123. }
  124. ret = append(ret, ws)
  125. }
  126. return ret
  127. }
  128. func (w *Workspaces) Get(id uuid.UUID) (ws WorkspaceAndRole) {
  129. res := w.Do(w.Sling().Get("/workspace/" + url.PathEscape(id.String())))
  130. if ok := res.Parse(&ws); !ok {
  131. panic(res)
  132. }
  133. if (ws.ParentID != uuid.UUID{}) {
  134. ws.Parent = w.Get(ws.ParentID).Name
  135. }
  136. if (ws.RoleVia != uuid.UUID{}) {
  137. ws.Via = w.Get(ws.ParentID).Name
  138. }
  139. return ws
  140. }
  141. func (w *Workspaces) GetByName(name string) (ws WorkspaceAndRole) {
  142. res := w.Do(w.Sling().Get("/workspace/" + url.PathEscape(name)))
  143. if ok := res.Parse(&ws); !ok {
  144. panic(res)
  145. }
  146. if (ws.ParentID != uuid.UUID{}) {
  147. ws.Parent = w.Get(ws.ParentID).Name
  148. }
  149. if (ws.RoleVia != uuid.UUID{}) {
  150. ws.Via = w.Get(ws.ParentID).Name
  151. }
  152. return ws
  153. }
  154. func (w *Workspaces) Create(parent string, sub string, desc string) (ws WorkspaceAndRole) {
  155. uri := fmt.Sprintf(
  156. "/workspace/%s/child",
  157. url.PathEscape(parent),
  158. )
  159. payload := make(map[string]string)
  160. payload["name"] = sub
  161. if desc != "" {
  162. payload["description"] = desc
  163. }
  164. res := w.Do(
  165. w.Sling().New().Post(uri).
  166. Set("Content-Type", "application/json").
  167. BodyJSON(payload),
  168. )
  169. if ok := res.Parse(&ws); !ok {
  170. panic(res)
  171. }
  172. if (ws.ParentID != uuid.UUID{}) {
  173. ws.Parent = w.Get(ws.ParentID).Name
  174. }
  175. if (ws.RoleVia != uuid.UUID{}) {
  176. ws.Via = w.Get(ws.ParentID).Name
  177. }
  178. return ws
  179. }
  180. /***/
  181. type WorkspaceUser struct {
  182. ID uuid.UUID `json:"id"`
  183. Name string `json:"name"`
  184. Email string `json:"email"`
  185. Role string `json:"role"`
  186. RoleVia uuid.UUID `json:"role_via"`
  187. Via string `json:"via"`
  188. }
  189. type WorkspaceUsers []WorkspaceUser
  190. func (w WorkspaceUsers) String() string {
  191. if API.JsonOnly {
  192. return API.AsJSON(w)
  193. }
  194. tableString := &strings.Builder{}
  195. table := tablewriter.NewWriter(tableString)
  196. TableToMarkdown(table)
  197. table.SetHeader([]string{
  198. "Name",
  199. "Email",
  200. "Role",
  201. "Role Via",
  202. })
  203. sort.Sort(w)
  204. for _, user := range w {
  205. table.Append([]string{
  206. user.Name,
  207. user.Email,
  208. user.Role,
  209. user.Via,
  210. })
  211. }
  212. table.Render()
  213. return tableString.String()
  214. }
  215. func (w WorkspaceUsers) Len() int {
  216. return len(w)
  217. }
  218. func (w WorkspaceUsers) Swap(i, j int) {
  219. w[i], w[j] = w[j], w[i]
  220. }
  221. func (w WorkspaceUsers) Less(i, j int) bool {
  222. return w[i].Name < w[j].Name
  223. }
  224. /***/
  225. func (w *Workspaces) GetUsers(name string) WorkspaceUsers {
  226. list := make(WorkspaceUsers, 0)
  227. users := make(WorkspaceUsers, 0)
  228. url := fmt.Sprintf(
  229. "/workspace/%s/user",
  230. url.PathEscape(name),
  231. )
  232. res := w.Do(w.Sling().Get(url))
  233. if ok := res.Parse(&list); !ok {
  234. panic(res)
  235. }
  236. for _, u := range list {
  237. if (u.RoleVia != uuid.UUID{}) {
  238. u.Via = w.Get(u.RoleVia).Name
  239. }
  240. users = append(users, u)
  241. }
  242. return users
  243. }
  244. func (w *Workspaces) AddOrModifyUser(workspace string, email string, role string, sendEmail bool) bool {
  245. payload := make(map[string]string)
  246. if email == "" {
  247. panic(errors.New("email address is required"))
  248. } else {
  249. payload["email"] = email
  250. }
  251. if role == "" {
  252. payload["role"] = "ro"
  253. } else {
  254. payload["role"] = role
  255. }
  256. params := make(map[string]int)
  257. if sendEmail {
  258. params["send_email"] = 1
  259. } else {
  260. params["send_email"] = 0
  261. }
  262. uri := fmt.Sprintf(
  263. "/workspace/%s/user",
  264. url.PathEscape(workspace),
  265. )
  266. p := struct {
  267. Email string `json:"email"`
  268. Role string `json:"role"`
  269. }{payload["email"], payload["role"]}
  270. q := struct {
  271. SendEmail int `url:"send_mail"`
  272. }{params["send_email"]}
  273. res := w.Do(
  274. w.Sling().Post(uri).
  275. Set("Content-Type", "application/json").
  276. BodyJSON(p).
  277. QueryStruct(q),
  278. )
  279. // NOTE: at time of writing, the only possible success response is a 204.
  280. // Everything else is a 400 or above. Here, that translates to a panic if
  281. // we didn't get a 20x. if we got a 20x, it's a 204. In the end, this will
  282. // return true or panic.
  283. return res.StatusCode() == 204
  284. }
  285. func (w *Workspaces) RemoveUser(workspace string, email string, sendEmail bool) bool {
  286. params := make(map[string]int)
  287. if sendEmail {
  288. params["send_email"] = 1
  289. } else {
  290. params["send_email"] = 0
  291. }
  292. q := struct {
  293. SendEmail int `url:"send_mail"`
  294. }{params["send_email"]}
  295. uri := fmt.Sprintf(
  296. "/workspace/%s/user/%s",
  297. url.PathEscape(workspace),
  298. url.PathEscape(email),
  299. )
  300. res := w.Do(w.Sling().Delete(uri).QueryStruct(q))
  301. // NOTE: at time of writing, the only possible success response is a 204.
  302. // Everything else is a 400 or above. Here, that translates to a panic if
  303. // we didn't get a 200. if we got a 200, it's a 204. In the end, this will
  304. // return true or panic.
  305. return res.StatusCode() == 204
  306. }
  307. /***/
  308. func (w *Workspaces) GetDevices(name string, health string, validated *bool) DeviceList {
  309. var opts interface{}
  310. valid := 0
  311. if (validated != nil) && *validated {
  312. valid = 1
  313. }
  314. if (health != "") && (validated != nil) {
  315. opts = struct {
  316. Health string `url:"health"`
  317. Validated int `url:"validated"`
  318. }{url.PathEscape(health), valid}
  319. } else if health != "" {
  320. opts = struct {
  321. Health string `url:"health"`
  322. }{url.PathEscape(health)}
  323. } else if validated != nil {
  324. opts = struct {
  325. Validated int `url:"validated"`
  326. }{valid}
  327. }
  328. url := fmt.Sprintf(
  329. "/workspace/%s/device",
  330. url.PathEscape(name),
  331. )
  332. devices := make(DeviceList, 0)
  333. res := w.Do(w.Sling().Get(url).QueryStruct(opts))
  334. if ok := res.Parse(&devices); !ok {
  335. panic(res)
  336. }
  337. return devices
  338. }
  339. /***/
  340. func (w *Workspaces) GetDirectChildren(name string) WorkspaceAndRoles {
  341. children := w.GetChildren(name)
  342. directs := make(WorkspaceAndRoles, 0)
  343. for _, child := range children {
  344. if child.Parent == name {
  345. directs = append(directs, child)
  346. }
  347. }
  348. return directs
  349. }
  350. func (w *Workspaces) GetChildren(name string) WorkspaceAndRoles {
  351. list := make(WorkspaceAndRoles, 0)
  352. uri := fmt.Sprintf("/workspace/%s/child", url.PathEscape(name))
  353. res := w.Do(w.Sling().Get(uri))
  354. if ok := res.Parse(&list); !ok {
  355. panic(res)
  356. }
  357. ret := make(WorkspaceAndRoles, 0)
  358. cache := make(map[uuid.UUID]string)
  359. for _, ws := range list {
  360. if (ws.ParentID != uuid.UUID{}) {
  361. if _, ok := cache[ws.ParentID]; !ok {
  362. cache[ws.ParentID] = w.Get(ws.ParentID).Name
  363. }
  364. ws.Parent = cache[ws.ParentID]
  365. }
  366. if (ws.RoleVia != uuid.UUID{}) {
  367. if _, ok := cache[ws.RoleVia]; !ok {
  368. cache[ws.RoleVia] = w.Get(ws.RoleVia).Name
  369. }
  370. ws.Via = cache[ws.RoleVia]
  371. }
  372. ret = append(ret, ws)
  373. }
  374. return ret
  375. }
  376. /***/
  377. // The DeviceProgress element is a map where the string is 'valid' or of the
  378. // 'device_health' type containing 'error', 'fail', 'unknown', 'pass'
  379. type WorkspaceRackSummary struct {
  380. ID uuid.UUID `json:"id"`
  381. Name string `json:"name"`
  382. Phase string `json:"phase"`
  383. RoleName string `json:"role_name"`
  384. RackSize int `json:"rack_size"`
  385. DeviceProgress map[string]int `json:"device_progress"`
  386. }
  387. type WorkspaceRackSummaries map[string][]WorkspaceRackSummary
  388. // The AZ gets lost in this conversion
  389. func (summaries WorkspaceRackSummaries) Slice() []WorkspaceRackSummary {
  390. s := make([]WorkspaceRackSummary, 0)
  391. for _, values := range summaries {
  392. s = append(s, values...)
  393. }
  394. return s
  395. }
  396. func (summaries WorkspaceRackSummaries) String() string {
  397. if API.JsonOnly {
  398. return API.AsJSON(summaries)
  399. }
  400. var output string
  401. keys := make([]string, 0)
  402. for az := range summaries {
  403. keys = append(keys, az)
  404. }
  405. sort.Strings(keys)
  406. for _, az := range keys {
  407. for _, summary := range summaries[az] {
  408. type status struct {
  409. Status string
  410. Count int
  411. }
  412. statusii := make([]*status, 0)
  413. statusStrs := make([]string, 0)
  414. for str := range summary.DeviceProgress {
  415. statusStrs = append(statusStrs, str)
  416. }
  417. sort.Strings(statusStrs)
  418. for _, statusStr := range statusStrs {
  419. statusii = append(statusii, &status{
  420. Status: statusStr,
  421. Count: summary.DeviceProgress[statusStr],
  422. })
  423. }
  424. s := struct {
  425. WorkspaceRackSummary
  426. AZ string
  427. Statuses []*status
  428. }{summary, az, statusii}
  429. t, err := NewTemplate().Parse(rackSummaryTemplate)
  430. if err != nil {
  431. panic(err)
  432. }
  433. buf := new(bytes.Buffer)
  434. if err := t.Execute(buf, s); err != nil {
  435. panic(err)
  436. }
  437. output = output + buf.String()
  438. }
  439. }
  440. return output
  441. }
  442. /****/
  443. func (w *Workspaces) GetRackSummaries(name string) WorkspaceRackSummaries {
  444. summaries := make(WorkspaceRackSummaries)
  445. url := fmt.Sprintf(
  446. "/workspace/%s/rack",
  447. url.PathEscape(name),
  448. )
  449. res := w.Do(w.Sling().Get(url))
  450. if ok := res.Parse(&summaries); !ok {
  451. panic(res)
  452. }
  453. return summaries
  454. }
  455. /***/
  456. type WorkspaceRelay struct {
  457. ID string `json:"id"`
  458. Name string `json:"name,omitempty"`
  459. Version string `json:"version,omitempty"`
  460. IpAddr string `json:"ipaddr,omitempty"`
  461. SshPort int `json:"ssh_port,omitempty"`
  462. Created time.Time `json:"created"`
  463. Updated time.Time `json:"updated"`
  464. LastSeen time.Time `json:"last_seen"`
  465. NumDevices int `json:"num_devices"`
  466. Location struct {
  467. RackID uuid.UUID `json:"rack_id"`
  468. RackName string `json:"rack_name"`
  469. RackUnitStart int `json:"rack_unit_start"`
  470. RoleName string `json:"role_name"`
  471. AZ string `json:"az"`
  472. } `json:"location"`
  473. }
  474. func (w WorkspaceRelay) String() string {
  475. if API.JsonOnly {
  476. return API.AsJSON(w)
  477. }
  478. t, err := NewTemplate().Parse(workspaceRelayTemplate)
  479. if err != nil {
  480. panic(err)
  481. }
  482. buf := new(bytes.Buffer)
  483. if err := t.Execute(buf, w); err != nil {
  484. panic(err)
  485. }
  486. return buf.String()
  487. }
  488. type WorkspaceRelays []WorkspaceRelay
  489. func (w WorkspaceRelays) Len() int {
  490. return len(w)
  491. }
  492. func (w WorkspaceRelays) Swap(i, j int) {
  493. w[i], w[j] = w[j], w[i]
  494. }
  495. func (w WorkspaceRelays) Less(i, j int) bool {
  496. return w[i].LastSeen.Unix() < w[j].LastSeen.Unix()
  497. }
  498. func (w WorkspaceRelays) String() string {
  499. sort.Sort(w)
  500. if API.JsonOnly {
  501. return API.AsJSON(w)
  502. }
  503. tableString := &strings.Builder{}
  504. table := tablewriter.NewWriter(tableString)
  505. TableToMarkdown(table)
  506. table.SetHeader([]string{
  507. "ID",
  508. "Version",
  509. "IP",
  510. "Port",
  511. "Last Seen",
  512. "Location",
  513. })
  514. for _, r := range w {
  515. rackID := ""
  516. if (r.Location.RackID != uuid.UUID{}) {
  517. rackID = fmt.Sprintf(
  518. "[%s]",
  519. CutUUID(r.Location.RackID.String()),
  520. )
  521. }
  522. location := fmt.Sprintf(
  523. "%s %s - RU %d %s",
  524. r.Location.AZ,
  525. r.Location.RackName,
  526. r.Location.RackUnitStart,
  527. rackID,
  528. )
  529. table.Append([]string{
  530. r.ID,
  531. r.Version,
  532. r.IpAddr,
  533. strconv.Itoa(r.SshPort),
  534. TimeStr(r.LastSeen),
  535. location,
  536. })
  537. }
  538. table.Render()
  539. return tableString.String()
  540. }
  541. func (w *Workspaces) GetRelays(name string) WorkspaceRelays {
  542. relays := make(WorkspaceRelays, 0)
  543. uri := fmt.Sprintf(
  544. "/workspace/%s/relay",
  545. url.PathEscape(name),
  546. )
  547. res := w.Do(w.Sling().Get(uri))
  548. if ok := res.Parse(&relays); !ok {
  549. panic(res)
  550. }
  551. return relays
  552. }
  553. func (w *Workspaces) GetRelayDevices(workspace string, relay string) DeviceList {
  554. devices := make(DeviceList, 0)
  555. uri := fmt.Sprintf(
  556. "/workspace/%s/relay/%s/device",
  557. url.PathEscape(workspace),
  558. url.PathEscape(relay),
  559. )
  560. res := w.Do(w.Sling().Get(uri))
  561. if ok := res.Parse(&devices); !ok {
  562. panic(res)
  563. }
  564. return devices
  565. }
  566. /******/
  567. func (w *Workspaces) AddRack(workspace string, rackID uuid.UUID) WorkspaceRackSummaries {
  568. // I really explictly am not supporting the full functionality of this
  569. // endpoint. Tehnically, it supports updating the rack's serial number and
  570. // asset tag at the same time you add it to the workspace. That's.. yeah.
  571. // If you want to change those fields, use the function for updating a
  572. // rack's data, not this one.
  573. uri := fmt.Sprintf(
  574. "/workspace/%s/rack",
  575. url.PathEscape(workspace),
  576. )
  577. payload := struct {
  578. ID string `json:"id"`
  579. }{rackID.String()}
  580. // Ignoring the return because we're about to pivot into a different call.
  581. // If this call errors out, it'll panic on its own
  582. _ = w.Do(
  583. w.Sling().New().Post(uri).
  584. Set("Content-Type", "application/json").
  585. BodyJSON(payload),
  586. )
  587. return w.GetRackSummaries(workspace)
  588. }
  589. func (w *Workspaces) RemoveRack(workspace string, rackID uuid.UUID) WorkspaceRackSummaries {
  590. uri := fmt.Sprintf(
  591. "/workspace/%s/rack/%s",
  592. url.PathEscape(workspace),
  593. url.PathEscape(rackID.String()),
  594. )
  595. // Ignoring the return because we're about to pivot into a different call.
  596. // If this call errors out, it'll panic on its own
  597. _ = w.Do(w.Sling().New().Delete(uri))
  598. return w.GetRackSummaries(workspace)
  599. }
  600. func (w *Workspaces) FindRackID(workspace string, id string) (bool, uuid.UUID) {
  601. summaries := make([]WorkspaceRackSummary, 0)
  602. summaries = append(
  603. summaries,
  604. w.GetRackSummaries(workspace).Slice()...,
  605. )
  606. ws := w.GetByName(workspace)
  607. if ws.Parent != "" {
  608. summaries = append(
  609. summaries,
  610. w.GetRackSummaries(ws.Parent).Slice()...,
  611. )
  612. }
  613. ids := make([]uuid.UUID, 0)
  614. for _, s := range summaries {
  615. ids = append(ids, s.ID)
  616. }
  617. return FindUUID(id, ids)
  618. }
  619. /******/
  620. func init() {
  621. App.Command("workspaces", "Get a list of all workspaces you have access to", func(cmd *cli.Cmd) {
  622. cmd.Action = func() {
  623. fmt.Println(API.Workspaces().GetAll())
  624. }
  625. })
  626. App.Command("workspace", "Deal with a single workspace", func(cmd *cli.Cmd) {
  627. var workspaceName string
  628. workspaceNameArg := cmd.StringArg(
  629. "NAME",
  630. "",
  631. "The string name of the workspace")
  632. cmd.Spec = "NAME"
  633. cmd.Before = func() {
  634. workspaceName = *workspaceNameArg
  635. // TODO(sungo): should we verify that the workspace exists?
  636. }
  637. cmd.Command("get", "Get information about a single workspace, using its name", func(cmd *cli.Cmd) {
  638. cmd.Action = func() {
  639. fmt.Println(API.Workspaces().GetByName(workspaceName))
  640. }
  641. })
  642. cmd.Command("create", "Create a new subworkspace", func(cmd *cli.Cmd) {
  643. nameArg := cmd.StringArg(
  644. "SUB",
  645. "",
  646. "Name of the new subworkspace",
  647. )
  648. descOpt := cmd.StringOpt(
  649. "description desc",
  650. "",
  651. "A description of the workspace",
  652. )
  653. cmd.Spec = "SUB [OPTIONS]"
  654. cmd.Action = func() {
  655. fmt.Println(API.Workspaces().Create(
  656. workspaceName,
  657. *nameArg,
  658. *descOpt,
  659. ))
  660. }
  661. })
  662. cmd.Command("add", "Add various structures to a single workspace", func(cmd *cli.Cmd) {
  663. cmd.Command("user", "Add a user to a workspace", func(cmd *cli.Cmd) {
  664. userEmailArg := cmd.StringArg(
  665. "EMAIL",
  666. "",
  667. "The email of the user to add to the workspace. Does *not* create the user",
  668. )
  669. roleOpt := cmd.StringOpt(
  670. "role",
  671. "ro",
  672. "The role for the user. One of: "+prettyWorkspaceRoleList(),
  673. )
  674. sendEmailOpt := cmd.BoolOpt(
  675. "send-email",
  676. true,
  677. "Send email to the target user, notifying them of the change",
  678. )
  679. cmd.Spec = "EMAIL [OPTIONS]"
  680. cmd.Action = func() {
  681. if !okWorkspaceRole(*roleOpt) {
  682. panic(fmt.Errorf(
  683. "'role' value must be one of: %s",
  684. prettyWorkspaceRoleList(),
  685. ))
  686. }
  687. if ok := API.Workspaces().AddOrModifyUser(
  688. workspaceName,
  689. *userEmailArg,
  690. *roleOpt,
  691. *sendEmailOpt,
  692. ); ok {
  693. fmt.Println(API.Workspaces().GetUsers(workspaceName))
  694. } else {
  695. // It should be impossible to reach this
  696. // code as the lower code panics in all
  697. // known failure conditions.
  698. panic(errors.New("failure"))
  699. }
  700. }
  701. })
  702. cmd.Command("rack", "Add a rack to a workspace", func(cmd *cli.Cmd) {
  703. idArg := cmd.StringArg(
  704. "UUID",
  705. "",
  706. "The UUID of the rack to add. Short UUIDs (first segment) accepted",
  707. )
  708. cmd.Spec = "UUID"
  709. cmd.Action = func() {
  710. var rackID uuid.UUID
  711. var ok bool
  712. if ok, rackID = API.Workspaces().FindRackID(workspaceName, *idArg); !ok {
  713. panic(errors.New("could not locate the rack in either this workspace or its parent"))
  714. }
  715. fmt.Println(API.Workspaces().AddRack(
  716. workspaceName,
  717. rackID,
  718. ))
  719. }
  720. })
  721. })
  722. cmd.Command("update", "Update various structures in a single workspace", func(cmd *cli.Cmd) {
  723. cmd.Command("user", "Update a user in a workspace", func(cmd *cli.Cmd) {
  724. userEmailArg := cmd.StringArg(
  725. "EMAIL",
  726. "",
  727. "The email of the user to modify",
  728. )
  729. roleOpt := cmd.StringOpt(
  730. "role",
  731. "ro",
  732. "The role for the user. One of: "+prettyWorkspaceRoleList(),
  733. )
  734. sendEmailOpt := cmd.BoolOpt(
  735. "send-email",
  736. true,
  737. "Send email to the target user, notifying them of the change",
  738. )
  739. cmd.Spec = "EMAIL [OPTIONS]"
  740. cmd.Action = func() {
  741. if !okWorkspaceRole(*roleOpt) {
  742. panic(fmt.Errorf(
  743. "'role' value must be one of: %s",
  744. prettyWorkspaceRoleList(),
  745. ))
  746. }
  747. if ok := API.Workspaces().AddOrModifyUser(
  748. workspaceName,
  749. *userEmailArg,
  750. *roleOpt,
  751. *sendEmailOpt,
  752. ); ok {
  753. fmt.Println(API.Workspaces().GetUsers(workspaceName))
  754. } else {
  755. // It should be impossible to reach this
  756. // code as the lower code panics in all
  757. // known failure conditions.
  758. panic(errors.New("failure"))
  759. }
  760. }
  761. })
  762. })
  763. cmd.Command("remove rm", "Remove various structures from a single workspace", func(cmd *cli.Cmd) {
  764. cmd.Command("user", "Remove a user from a workspace", func(cmd *cli.Cmd) {
  765. userEmailArg := cmd.StringArg(
  766. "EMAIL",
  767. "",
  768. "The email of the user to modify",
  769. )
  770. sendEmailOpt := cmd.BoolOpt(
  771. "send-email",
  772. true,
  773. "Send email to the target user, notifying them of the change",
  774. )
  775. cmd.Spec = "EMAIL [OPTIONS]"
  776. cmd.Action = func() {
  777. if ok := API.Workspaces().RemoveUser(
  778. workspaceName,
  779. *userEmailArg,
  780. *sendEmailOpt,
  781. ); ok {
  782. fmt.Println(API.Workspaces().GetUsers(workspaceName))
  783. } else {
  784. // It should be impossible to reach this
  785. // code as the lower code panics in all
  786. // known failure conditions.
  787. panic(errors.New("failure"))
  788. }
  789. }
  790. })
  791. cmd.Command("rack", "Remove a rack from a workspace", func(cmd *cli.Cmd) {
  792. idArg := cmd.StringArg(
  793. "UUID",
  794. "",
  795. "The UUID of the rack to remove. Short UUIDs (first segment) accepted",
  796. )
  797. cmd.Spec = "UUID"
  798. cmd.Action = func() {
  799. var id uuid.UUID
  800. var err error
  801. if id, err = uuid.FromString(*idArg); err != nil {
  802. if ok, rackID := API.Workspaces().FindRackID(workspaceName, *idArg); ok {
  803. id = rackID
  804. } else {
  805. panic(errors.New("could not locate the rack in either this workspace or its parent"))
  806. }
  807. }
  808. if (id == uuid.UUID{}) {
  809. panic(errors.New("could not locate the rack in either this workspace or its parent"))
  810. }
  811. fmt.Println(API.Workspaces().RemoveRack(workspaceName, id))
  812. }
  813. })
  814. })
  815. cmd.Command("relays", "Get a list of relays assigned to a single workspace", func(cmd *cli.Cmd) {
  816. cmd.Action = func() {
  817. fmt.Println(API.Workspaces().GetRelays(workspaceName))
  818. }
  819. })
  820. cmd.Command("relay", "Deal with a single relay", func(cmd *cli.Cmd) {
  821. cmd.Command("get", "Get information about a single relay", func(cmd *cli.Cmd) {
  822. relayArg := cmd.StringArg(
  823. "RELAY",
  824. "",
  825. "ID of the relay",
  826. )
  827. cmd.Spec = "RELAY"
  828. cmd.Action = func() {
  829. relays := API.Workspaces().GetRelays(workspaceName)
  830. for _, relay := range relays {
  831. if relay.ID != *relayArg {
  832. continue
  833. }
  834. fmt.Println(relay)
  835. return
  836. }
  837. panic(errors.New("relay not found"))
  838. }
  839. })
  840. cmd.Command("devices", "Get the device list for a relay", func(cmd *cli.Cmd) {
  841. relayArg := cmd.StringArg(
  842. "RELAY",
  843. "",
  844. "ID of the relay",
  845. )
  846. cmd.Spec = "RELAY"
  847. cmd.Action = func() {
  848. fmt.Println(API.Workspaces().GetRelayDevices(
  849. workspaceName,
  850. *relayArg,
  851. ))
  852. }
  853. })
  854. })
  855. cmd.Command("children subs", "Get a list of a workspace's children", func(cmd *cli.Cmd) {
  856. allOpt := cmd.BoolOpt(
  857. "all",
  858. false,
  859. "Retrieve all children, not just the direct lineage",
  860. )
  861. // TODO(sungo): tree mode?
  862. cmd.Action = func() {
  863. if *allOpt {
  864. fmt.Println(API.Workspaces().GetChildren(workspaceName))
  865. } else {
  866. fmt.Println(API.Workspaces().GetDirectChildren(workspaceName))
  867. }
  868. }
  869. })
  870. cmd.Command("devices", "Get a list of devices in a single workspace, by name", func(cmd *cli.Cmd) {
  871. healthOpt := cmd.StringOpt(
  872. "health",
  873. "",
  874. "Filter by the 'health' field. Value must be one of: "+prettyDeviceHealthList(),
  875. )
  876. var validatedSetByUser bool
  877. validatedOpt := cmd.Bool(cli.BoolOpt{
  878. Name: "validated",
  879. Value: false,
  880. Desc: "Filter by the 'validated' field",
  881. SetByUser: &validatedSetByUser,
  882. })
  883. cmd.Action = func() {
  884. if !validatedSetByUser {
  885. validatedOpt = nil
  886. }
  887. if *healthOpt != "" {
  888. if !okHealth(*healthOpt) {
  889. panic(fmt.Errorf("'health' value must be one of: %s", prettyDeviceHealthList()))
  890. }
  891. }
  892. fmt.Println(API.Workspaces().GetDevices(
  893. workspaceName,
  894. *healthOpt,
  895. validatedOpt,
  896. ))
  897. }
  898. })
  899. cmd.Command("users", "Operate on the users assigned to a workspace", func(cmd *cli.Cmd) {
  900. cmd.Action = func() {
  901. fmt.Println(API.Workspaces().GetUsers(workspaceName))
  902. }
  903. })
  904. cmd.Command("racks", "Get a progress summary for each rack", func(cmd *cli.Cmd) {
  905. phaseOpt := cmd.StringOpt(
  906. "phase",
  907. "",
  908. "Filter based on phase name",
  909. )
  910. roleOpt := cmd.StringOpt(
  911. "role",
  912. "",
  913. "Filter on role name",
  914. )
  915. cmd.Action = func() {
  916. ret := API.Workspaces().GetRackSummaries(workspaceName)
  917. summaries := make(WorkspaceRackSummaries)
  918. for az, summary := range ret {
  919. for _, s := range summary {
  920. save := true
  921. if *phaseOpt != "" {
  922. if s.Phase != *phaseOpt {
  923. save = false
  924. }
  925. }
  926. if *roleOpt != "" {
  927. if s.RoleName != *roleOpt {
  928. save = false
  929. }
  930. }
  931. if save {
  932. if _, ok := summaries[az]; !ok {
  933. summaries[az] = make([]WorkspaceRackSummary, 0)
  934. }
  935. summaries[az] = append(summaries[az], s)
  936. }
  937. }
  938. }
  939. fmt.Println(summaries)
  940. }
  941. })
  942. })
  943. }