Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(272)

Side by Side Diff: environs/tools_test.go

Issue 8727044: environs: FindInstanceTools, FindBootstrapTools
Patch Set: environs: FindInstanceTools, FindBootstrapTools Created 11 years, 12 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « environs/tools.go ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 package environs_test 1 package environs_test
2 2
3 import ( 3 import (
4 "io/ioutil" 4 "io/ioutil"
5 . "launchpad.net/gocheck" 5 . "launchpad.net/gocheck"
6 "launchpad.net/juju-core/constraints"
6 "launchpad.net/juju-core/environs" 7 "launchpad.net/juju-core/environs"
7 "launchpad.net/juju-core/environs/dummy" 8 "launchpad.net/juju-core/environs/dummy"
8 envtesting "launchpad.net/juju-core/environs/testing" 9 envtesting "launchpad.net/juju-core/environs/testing"
9 "launchpad.net/juju-core/environs/tools" 10 "launchpad.net/juju-core/environs/tools"
10 "launchpad.net/juju-core/state" 11 "launchpad.net/juju-core/state"
11 "launchpad.net/juju-core/testing" 12 "launchpad.net/juju-core/testing"
12 "launchpad.net/juju-core/version" 13 "launchpad.net/juju-core/version"
13 "net/http" 14 "net/http"
14 "strings" 15 "strings"
15 ) 16 )
16 17
17 type ToolsSuite struct { 18 type ToolsSuite struct {
18 env environs.Environ 19 env environs.Environ
19 testing.LoggingSuite 20 testing.LoggingSuite
20 » dataDir string 21 » origCurrentVersion version.Binary
21 } 22 }
22 23
23 var _ = Suite(&ToolsSuite{}) 24 var _ = Suite(&ToolsSuite{})
24 25
25 func (s *ToolsSuite) SetUpTest(c *C) { 26 func (s *ToolsSuite) SetUpTest(c *C) {
26 s.LoggingSuite.SetUpTest(c) 27 s.LoggingSuite.SetUpTest(c)
27 » env, err := environs.NewFromAttrs(map[string]interface{}{ 28 » s.origCurrentVersion = version.Current
29 » s.Reset(c, nil)
30 }
31
32 func (s *ToolsSuite) TearDownTest(c *C) {
33 » dummy.Reset()
34 » version.Current = s.origCurrentVersion
35 » s.LoggingSuite.TearDownTest(c)
36 }
37
38 func (s *ToolsSuite) Reset(c *C, attrs map[string]interface{}) {
39 » version.Current = s.origCurrentVersion
40 » dummy.Reset()
41 » final := map[string]interface{}{
28 "name": "test", 42 "name": "test",
29 "type": "dummy", 43 "type": "dummy",
30 "state-server": false, 44 "state-server": false,
31 "authorized-keys": "i-am-a-key", 45 "authorized-keys": "i-am-a-key",
32 "ca-cert": testing.CACert, 46 "ca-cert": testing.CACert,
33 "ca-private-key": "", 47 "ca-private-key": "",
34 » }) 48 » }
49 » for k, v := range attrs {
50 » » final[k] = v
51 » }
52 » env, err := environs.NewFromAttrs(final)
35 c.Assert(err, IsNil) 53 c.Assert(err, IsNil)
36 s.env = env 54 s.env = env
37 s.dataDir = c.MkDir()
38 envtesting.RemoveAllTools(c, s.env) 55 envtesting.RemoveAllTools(c, s.env)
39 } 56 }
40 57
41 func (s *ToolsSuite) TearDownTest(c *C) {
42 dummy.Reset()
43 s.LoggingSuite.TearDownTest(c)
44 }
45
46 func toolsStorageName(vers string) string { 58 func toolsStorageName(vers string) string {
47 return tools.StorageName(version.Binary{ 59 return tools.StorageName(version.Binary{
48 Number: version.MustParse(vers), 60 Number: version.MustParse(vers),
49 Series: version.CurrentSeries(), 61 Series: version.CurrentSeries(),
50 Arch: version.CurrentArch(), 62 Arch: version.CurrentArch(),
51 }) 63 })
52 } 64 }
53 65
54 type toolsSpec struct { 66 type toolsSpec struct {
55 version string 67 version string
(...skipping 379 matching lines...) Expand 10 before | Expand all | Expand 10 after
435 tools = environs.BestTools(test.list, test.vers, environs.DevVer sion|environs.CompatVersion) 447 tools = environs.BestTools(test.list, test.vers, environs.DevVer sion|environs.CompatVersion)
436 c.Check(tools, DeepEquals, test.expectDev) 448 c.Check(tools, DeepEquals, test.expectDev)
437 tools = environs.BestTools(test.list, test.vers, environs.Highes tVersion|environs.CompatVersion) 449 tools = environs.BestTools(test.list, test.vers, environs.Highes tVersion|environs.CompatVersion)
438 c.Check(tools, DeepEquals, test.expectHighest) 450 c.Check(tools, DeepEquals, test.expectHighest)
439 tools = environs.BestTools(test.list, test.vers, environs.DevVer sion|environs.HighestVersion|environs.CompatVersion) 451 tools = environs.BestTools(test.list, test.vers, environs.DevVer sion|environs.HighestVersion|environs.CompatVersion)
440 c.Check(tools, DeepEquals, test.expectDevHighest) 452 c.Check(tools, DeepEquals, test.expectDevHighest)
441 } 453 }
442 } 454 }
443 455
444 var ( 456 var (
445 » v100 = version.MustParse("1.0.0") 457 » v100 = version.MustParse("1.0.0")
446 » v100p64 = version.MustParseBinary("1.0.0-precise-amd64") 458 » v100p64 = version.MustParseBinary("1.0.0-precise-amd64")
447 » v100p32 = version.MustParseBinary("1.0.0-precise-i386") 459 » v100p32 = version.MustParseBinary("1.0.0-precise-i386")
448 » v100q64 = version.MustParseBinary("1.0.0-quantal-amd64") 460 » v100p = []version.Binary{v100p64, v100p32}
449 » v100q32 = version.MustParseBinary("1.0.0-quantal-i386") 461
462 » v100q64 = version.MustParseBinary("1.0.0-quantal-amd64")
463 » v100q32 = version.MustParseBinary("1.0.0-quantal-i386")
464 » v100q = []version.Binary{v100q64, v100q32}
465 » v100all = append(v100p, v100q...)
466
467 » v1001 = version.MustParse("1.0.0.1")
450 v1001p64 = version.MustParseBinary("1.0.0.1-precise-amd64") 468 v1001p64 = version.MustParseBinary("1.0.0.1-precise-amd64")
451 » v100all = []version.Binary{v100p64, v100p32, v100q64, v100q32, v1001p64 } 469 » v100Xall = append(v100all, v1001p64)
452 470
453 v110 = version.MustParse("1.1.0") 471 v110 = version.MustParse("1.1.0")
454 v110p64 = version.MustParseBinary("1.1.0-precise-amd64") 472 v110p64 = version.MustParseBinary("1.1.0-precise-amd64")
455 v110p32 = version.MustParseBinary("1.1.0-precise-i386") 473 v110p32 = version.MustParseBinary("1.1.0-precise-i386")
456 v110p = []version.Binary{v110p64, v110p32} 474 v110p = []version.Binary{v110p64, v110p32}
457 475
458 v110q64 = version.MustParseBinary("1.1.0-quantal-amd64") 476 v110q64 = version.MustParseBinary("1.1.0-quantal-amd64")
459 v110q32 = version.MustParseBinary("1.1.0-quantal-i386") 477 v110q32 = version.MustParseBinary("1.1.0-quantal-i386")
460 » v110all = []version.Binary{v110p64, v110p32, v110q64, v110q32} 478 » v110q = []version.Binary{v110q64, v110q32}
479 » v110all = append(v110p, v110q...)
461 480
462 v120 = version.MustParse("1.2.0") 481 v120 = version.MustParse("1.2.0")
463 v120p64 = version.MustParseBinary("1.2.0-precise-amd64") 482 v120p64 = version.MustParseBinary("1.2.0-precise-amd64")
464 v120p32 = version.MustParseBinary("1.2.0-precise-i386") 483 v120p32 = version.MustParseBinary("1.2.0-precise-i386")
484 v120p = []version.Binary{v120p64, v120p32}
485
465 v120q64 = version.MustParseBinary("1.2.0-quantal-amd64") 486 v120q64 = version.MustParseBinary("1.2.0-quantal-amd64")
466 v120q32 = version.MustParseBinary("1.2.0-quantal-i386") 487 v120q32 = version.MustParseBinary("1.2.0-quantal-i386")
467 » v120all = []version.Binary{v120p64, v120p32, v120q64, v120q32} 488 » v120q = []version.Binary{v120q64, v120q32}
468 » v1all = append(v100all, append(v110all, v120all...)...) 489 » v120all = append(v120p, v120q...)
490 » v1all = append(v100Xall, append(v110all, v120all...)...)
469 491
470 v220 = version.MustParse("2.2.0") 492 v220 = version.MustParse("2.2.0")
471 v220p32 = version.MustParseBinary("2.2.0-precise-i386") 493 v220p32 = version.MustParseBinary("2.2.0-precise-i386")
472 v220p64 = version.MustParseBinary("2.2.0-precise-amd64") 494 v220p64 = version.MustParseBinary("2.2.0-precise-amd64")
473 v220q32 = version.MustParseBinary("2.2.0-quantal-i386") 495 v220q32 = version.MustParseBinary("2.2.0-quantal-i386")
474 v220q64 = version.MustParseBinary("2.2.0-quantal-amd64") 496 v220q64 = version.MustParseBinary("2.2.0-quantal-amd64")
475 v220all = []version.Binary{v220p64, v220p32, v220q64, v220q32} 497 v220all = []version.Binary{v220p64, v220p32, v220q64, v220q32}
476 vAll = append(v1all, v220all...) 498 vAll = append(v1all, v220all...)
477 ) 499 )
478 500
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
529 info: "private tools completely block public ones", 551 info: "private tools completely block public ones",
530 major: 1, 552 major: 1,
531 private: v220all, 553 private: v220all,
532 public: vAll, 554 public: vAll,
533 err: tools.ErrNoMatches, 555 err: tools.ErrNoMatches,
534 }} 556 }}
535 557
536 func (s *ToolsSuite) TestFindAvailableTools(c *C) { 558 func (s *ToolsSuite) TestFindAvailableTools(c *C) {
537 for i, test := range findAvailableToolsTests { 559 for i, test := range findAvailableToolsTests {
538 c.Logf("\ntest %d: %s", i, test.info) 560 c.Logf("\ntest %d: %s", i, test.info)
539 » » envtesting.RemoveAllTools(c, s.env) 561 » » s.Reset(c, nil)
540 private := s.uploadPrivate(c, test.private...) 562 private := s.uploadPrivate(c, test.private...)
541 public := s.uploadPublic(c, test.public...) 563 public := s.uploadPublic(c, test.public...)
542 actual, err := environs.FindAvailableTools(s.env, test.major) 564 actual, err := environs.FindAvailableTools(s.env, test.major)
543 if test.err != nil { 565 if test.err != nil {
544 if len(actual) > 0 { 566 if len(actual) > 0 {
545 c.Logf(actual.String()) 567 c.Logf(actual.String())
546 } 568 }
547 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r}) 569 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r})
548 continue 570 continue
549 } 571 }
550 source := private 572 source := private
551 if len(source) == 0 { 573 if len(source) == 0 {
552 // We only use the public bucket if the private one has *no* tools. 574 // We only use the public bucket if the private one has *no* tools.
553 source = public 575 source = public
554 } 576 }
555 expect := map[version.Binary]string{} 577 expect := map[version.Binary]string{}
556 for _, expected := range test.expect { 578 for _, expected := range test.expect {
557 expect[expected] = source[expected] 579 expect[expected] = source[expected]
558 } 580 }
559 c.Check(actual.URLs(), DeepEquals, expect) 581 c.Check(actual.URLs(), DeepEquals, expect)
560 } 582 }
561 } 583 }
562 584
585 var ensureAgentVersionTests = []struct {
586 info string
587 available []version.Binary
588 cliVersion version.Binary
589 defaultSeries string
590 agentVersion version.Number
591 development bool
592 constraints string
593 expect []version.Binary
594 err error
595 }{{
596 info: "no tools at all",
597 cliVersion: v100p64,
598 defaultSeries: "precise",
599 err: tools.ErrNoTools,
600 }, {
601 info: "released cli: use newest compatible release version",
602 available: vAll,
603 cliVersion: v100p64,
604 defaultSeries: "precise",
605 expect: v120p,
606 }, {
607 info: "released cli: cli arch ignored",
608 available: vAll,
609 cliVersion: v100p32,
610 defaultSeries: "precise",
611 expect: v120p,
612 }, {
613 info: "released cli: cli series ignored",
614 available: vAll,
615 cliVersion: v100q64,
616 defaultSeries: "precise",
617 expect: v120p,
618 }, {
619 info: "released cli: series taken from default-series",
620 available: v100Xall,
621 cliVersion: v100p64,
622 defaultSeries: "quantal",
623 expect: v100q,
624 }, {
625 info: "released cli: ignore close dev match",
626 available: v100Xall,
627 cliVersion: v120p64,
628 defaultSeries: "precise",
629 expect: v100p,
630 }, {
631 info: "released cli: use older release version if necessary",
632 available: v100Xall,
633 cliVersion: v120p64,
634 defaultSeries: "quantal",
635 expect: v100q,
636 }, {
637 info: "released cli: ignore irrelevant constraints",
638 available: v100Xall,
639 cliVersion: v100p64,
640 defaultSeries: "precise",
641 constraints: "mem=32G",
642 expect: v100p,
643 }, {
644 info: "released cli: filter by arch constraints",
645 available: v120all,
646 cliVersion: v100p64,
647 defaultSeries: "precise",
648 constraints: "arch=i386",
649 expect: []version.Binary{v120p32},
650 }, {
651 info: "released cli: specific released version",
652 available: vAll,
653 cliVersion: v120p64,
654 agentVersion: v100,
655 defaultSeries: "precise",
656 expect: v100p,
657 }, {
658 info: "released cli: specific dev version",
659 available: vAll,
660 cliVersion: v120p64,
661 agentVersion: v110,
662 defaultSeries: "precise",
663 expect: v110p,
664 }, {
665 info: "released cli: major upgrades bad",
666 available: v220all,
667 cliVersion: v100p64,
668 defaultSeries: "precise",
669 err: tools.ErrNoMatches,
670 }, {
671 info: "released cli: major downgrades bad",
672 available: v100Xall,
673 cliVersion: v220p64,
674 defaultSeries: "precise",
675 err: tools.ErrNoMatches,
676 }, {
677 info: "released cli: no matching series",
678 available: vAll,
679 cliVersion: v100p64,
680 defaultSeries: "raring",
681 err: tools.ErrNoMatches,
682 }, {
683 info: "released cli: no matching arches",
684 available: vAll,
685 cliVersion: v100p64,
686 defaultSeries: "precise",
687 constraints: "arch=arm",
688 err: tools.ErrNoMatches,
689 }, {
690 info: "released cli: specific bad major 1",
691 available: vAll,
692 cliVersion: v220p64,
693 agentVersion: v120,
694 defaultSeries: "precise",
695 err: tools.ErrNoMatches,
696 }, {
697 info: "released cli: specific bad major 2",
698 available: vAll,
699 cliVersion: v120p64,
700 agentVersion: v220,
701 defaultSeries: "precise",
702 err: tools.ErrNoMatches,
703 }, {
704 info: "released cli: ignore dev tools 1",
705 available: v110all,
706 cliVersion: v100p64,
707 defaultSeries: "precise",
708 err: tools.ErrNoMatches,
709 }, {
710 info: "released cli: ignore dev tools 2",
711 available: v110all,
712 cliVersion: v120p64,
713 defaultSeries: "precise",
714 err: tools.ErrNoMatches,
715 }, {
716 info: "released cli: ignore dev tools 3",
717 available: []version.Binary{v1001p64},
718 cliVersion: v100p64,
719 defaultSeries: "precise",
720 err: tools.ErrNoMatches,
721 }, {
722 info: "released cli with dev setting picks newest matching 1",
723 available: v100Xall,
724 cliVersion: v120q32,
725 defaultSeries: "precise",
726 development: true,
727 expect: []version.Binary{v1001p64},
728 }, {
729 info: "released cli with dev setting picks newest matching 2",
730 available: vAll,
731 cliVersion: v100q64,
732 defaultSeries: "precise",
733 development: true,
734 constraints: "arch=i386",
735 expect: []version.Binary{v120p32},
736 }, {
737 info: "released cli with dev setting respects agent-version",
738 available: vAll,
739 cliVersion: v100q32,
740 agentVersion: v1001,
741 defaultSeries: "precise",
742 development: true,
743 expect: []version.Binary{v1001p64},
744 }, {
745 info: "dev cli picks newest matching 1",
746 available: v100Xall,
747 cliVersion: v110q32,
748 defaultSeries: "precise",
749 expect: []version.Binary{v1001p64},
750 }, {
751 info: "dev cli picks newest matching 2",
752 available: vAll,
753 cliVersion: v110q64,
754 defaultSeries: "precise",
755 constraints: "arch=i386",
756 expect: []version.Binary{v120p32},
757 }, {
758 info: "dev cli respects agent-version",
759 available: vAll,
760 cliVersion: v110q32,
761 agentVersion: v1001,
762 defaultSeries: "precise",
763 expect: []version.Binary{v1001p64},
764 }}
765
766 func (s *ToolsSuite) TestEnsureAgentVersion(c *C) {
767 for i, test := range ensureAgentVersionTests {
768 c.Logf("\ntest %d: %s", i, test.info)
769 attrs := map[string]interface{}{
770 "development": test.development,
771 "default-series": test.defaultSeries,
772 }
773 if test.agentVersion != version.Zero {
774 attrs["agent-version"] = test.agentVersion.String()
775 }
776 s.Reset(c, attrs)
777 version.Current = test.cliVersion
778 available := s.uploadPrivate(c, test.available...)
779 if len(available) > 0 {
780 // These should never be chosen.
781 s.uploadPublic(c, vAll...)
782 }
783
784 cons := constraints.MustParse(test.constraints)
785 actual, err := environs.EnsureAgentVersion(s.env, cons)
786 if test.err != nil {
787 if len(actual) > 0 {
788 c.Logf(actual.String())
789 }
790 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r})
791 continue
792 }
793 expect := map[version.Binary]string{}
794 unique := map[version.Number]bool{}
795 for _, expected := range test.expect {
796 expect[expected] = available[expected]
797 unique[expected.Number] = true
798 }
799 c.Check(actual.URLs(), DeepEquals, expect)
800 for expectAgentVersion := range unique {
801 agentVersion, ok := s.env.Config().AgentVersion()
802 c.Check(ok, Equals, true)
803 c.Check(agentVersion, Equals, expectAgentVersion)
804 }
805 }
806 }
807
808 var findInstanceToolsTests = []struct {
809 info string
810 available []version.Binary
811 agentVersion version.Number
812 series string
813 constraints string
814 expect []version.Binary
815 err error
816 }{{
817 info: "nothing at all",
818 agentVersion: v120,
819 series: "precise",
820 err: tools.ErrNoTools,
821 }, {
822 info: "nothing matching 1",
823 available: v100Xall,
824 agentVersion: v120,
825 series: "precise",
826 err: tools.ErrNoMatches,
827 }, {
828 info: "nothing matching 2",
829 available: v120all,
830 agentVersion: v110,
831 series: "precise",
832 err: tools.ErrNoMatches,
833 }, {
834 info: "nothing matching 3",
835 available: v120q,
836 agentVersion: v120,
837 series: "precise",
838 err: tools.ErrNoMatches,
839 }, {
840 info: "nothing matching 4",
841 available: v120q,
842 agentVersion: v120,
843 series: "quantal",
844 constraints: "arch=arm",
845 err: tools.ErrNoMatches,
846 }, {
847 info: "actual match 1",
848 available: vAll,
849 agentVersion: v1001,
850 series: "precise",
851 expect: []version.Binary{v1001p64},
852 }, {
853 info: "actual match 2",
854 available: vAll,
855 agentVersion: v120,
856 series: "quantal",
857 expect: []version.Binary{v120q64, v120q32},
858 }, {
859 info: "actual match 3",
860 available: vAll,
861 agentVersion: v110,
862 series: "quantal",
863 constraints: "arch=i386",
864 expect: []version.Binary{v110q32},
865 }}
866
867 func (s *ToolsSuite) TestFindInstanceTools(c *C) {
868 for i, test := range findInstanceToolsTests {
869 c.Logf("\ntest %d: %s", i, test.info)
870 s.Reset(c, map[string]interface{}{
871 "agent-version": test.agentVersion.String(),
872 })
873 available := s.uploadPrivate(c, test.available...)
874 if len(available) > 0 {
875 // These should never be chosen.
876 s.uploadPublic(c, vAll...)
877 }
878
879 cons := constraints.MustParse(test.constraints)
880 actual, err := environs.FindInstanceTools(s.env, test.series, co ns)
881 if test.err != nil {
882 if len(actual) > 0 {
883 c.Logf(actual.String())
884 }
885 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r})
886 continue
887 }
888 expect := map[version.Binary]string{}
889 for _, expected := range test.expect {
890 expect[expected] = available[expected]
891 }
892 c.Check(actual.URLs(), DeepEquals, expect)
893 }
894 }
895
563 var findExactToolsTests = []struct { 896 var findExactToolsTests = []struct {
564 info string 897 info string
565 private []version.Binary 898 private []version.Binary
566 public []version.Binary 899 public []version.Binary
567 seek version.Binary 900 seek version.Binary
568 err error 901 err error
569 }{{ 902 }{{
570 info: "nothing available", 903 info: "nothing available",
571 seek: v100p64, 904 seek: v100p64,
572 err: tools.ErrNoTools, 905 err: tools.ErrNoTools,
(...skipping 18 matching lines...) Expand all
591 }, { 924 }, {
592 info: "exact match in public blocked by private", 925 info: "exact match in public blocked by private",
593 private: v110all, 926 private: v110all,
594 public: []version.Binary{v100p64}, 927 public: []version.Binary{v100p64},
595 seek: v100p64, 928 seek: v100p64,
596 err: tools.ErrNoMatches, 929 err: tools.ErrNoMatches,
597 }} 930 }}
598 931
599 func (s *ToolsSuite) TestFindExactTools(c *C) { 932 func (s *ToolsSuite) TestFindExactTools(c *C) {
600 for i, test := range findExactToolsTests { 933 for i, test := range findExactToolsTests {
601 » » c.Logf("\ntest %d", i) 934 » » c.Logf("\ntest %d: %s", i, test.info)
602 » » envtesting.RemoveAllTools(c, s.env) 935 » » s.Reset(c, nil)
603 private := s.uploadPrivate(c, test.private...) 936 private := s.uploadPrivate(c, test.private...)
604 public := s.uploadPublic(c, test.public...) 937 public := s.uploadPublic(c, test.public...)
605 actual, err := environs.FindExactTools(s.env, test.seek) 938 actual, err := environs.FindExactTools(s.env, test.seek)
606 if test.err == nil { 939 if test.err == nil {
607 c.Check(err, IsNil) 940 c.Check(err, IsNil)
608 c.Check(actual.Binary, Equals, test.seek) 941 c.Check(actual.Binary, Equals, test.seek)
609 source := private 942 source := private
610 if len(source) == 0 { 943 if len(source) == 0 {
611 // We only use the public bucket if the private one has *no* tools. 944 // We only use the public bucket if the private one has *no* tools.
612 source = public 945 source = public
613 } 946 }
614 c.Check(actual.URL, DeepEquals, source[actual.Binary]) 947 c.Check(actual.URL, DeepEquals, source[actual.Binary])
615 } else { 948 } else {
616 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r}) 949 c.Check(err, DeepEquals, &environs.NotFoundError{test.er r})
617 } 950 }
618 } 951 }
619 } 952 }
OLDNEW
« no previous file with comments | « environs/tools.go ('k') | no next file » | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b